Portal Community

The Five Core Principles

1. Credential Safety

All secrets (API keys, tokens, passwords) must flow through ICredentialResolver. Store only a credential ID in configuration — never the secret itself.

2. Tenant Isolation

Every data access and every external API call must be scoped to the current tenant. Use ITenantContext to read the tenant ID from the JWT on every request.

3. MCP Tool Schema Standards

Every MCP tool must expose a valid JSON Schema as its inputSchema. Required fields must be listed in the required array. No schema-free tools.

4. IOctopusPlugin Contract

Implement all three lifecycle methods: OnRegister, OnStartAsync, OnStopAsync. Register DI services in OnRegister, start background work in OnStartAsync.

5. Graceful Degradation

A plugin failure must never crash the Octopus host. Wrap external calls in try/catch, return structured error JSON from tool handlers, and log with structured context.

Principle 1: Credential Safety

The credential pattern is mandatory for every plugin that connects to an external service. Raw secrets in appsettings.json — even in environment variables — are a security violation.

// CORRECT — store only the credential ID
{
  "EmailPlugin": {
    "CredentialId": 30      // integer — resolved at runtime
  }
}

// WRONG — never store raw secrets
{
  "EmailPlugin": {
    "SmtpPassword": "hunter2"   // ❌ violates credential pattern
  }
}
// In OnStartAsync — resolve the secret at runtime
public async Task OnStartAsync(IOctopusPluginContext context)
{
    var config  = context.Config.GetSection<EmailPluginConfig>("EmailPlugin");
    var secret  = await context.CredentialResolver
                               .GetPasswordAsync(config.CredentialId);

    _smtpClient = new SmtpClient(config.SmtpHost)
    {
        Credentials = new NetworkCredential(config.SmtpUser, secret)
    };
}
Secret TypeStorageResolution
SMTP / IMAP passwordCredential store (ID in config)GetPasswordAsync(credentialId)
OAuth access tokenCredential store (ID in config)GetPasswordAsync(credentialId)
API key (SendGrid, HubSpot)Credential store (ID in config)GetPasswordAsync(credentialId)
Database connection stringCredential store (ID in config)GetPasswordAsync(credentialId)
Host / port / endpoint URLappsettings.json (safe)Direct config bind

Principle 2: Tenant Isolation

Multi-tenancy is not optional. Every external API call, every database query, and every cache entry must be keyed or filtered by tenant ID.

// Inject ITenantContext into your tool handler
public class CrmToolHandler
{
    private readonly ITenantContext _tenant;
    private readonly ICrmClient     _crm;

    public CrmToolHandler(ITenantContext tenant, ICrmClient crm)
    {
        _tenant = tenant;
        _crm    = crm;
    }

    public async Task<JsonElement> HandleGetContactAsync(JsonElement input)
    {
        var tenantId = _tenant.TenantId;   // read from JWT "tid" claim

        // Scope the query to this tenant's CRM instance
        var contact = await _crm.GetContactAsync(
            tenantId: tenantId,
            email:    input.GetProperty("email").GetString());

        return JsonSerializer.SerializeToElement(contact);
    }
}
Cross-tenant data leakage. If a tool handler reads data without checking ITenantContext.TenantId, any authenticated agent can access another tenant's contacts, emails, or purchase orders. This is a critical security violation.
Isolation LayerMechanismExample
EF Core entitiesGlobal query filter on TenantIdHasQueryFilter(e => e.TenantId == _tenant.TenantId)
External API callsTenant-scoped client or headerX-Tenant-Id header, or per-tenant OAuth token
In-memory cacheKey prefix with tenant ID$"{tenantId}:crm:contact:{email}"
File / blob storageTenant-scoped path or containertenants/{tenantId}/attachments/
Background workersCapture tenant at start, propagateOctopusHILActor pattern — TenantId in task payload

Principle 3: MCP Tool Schema Standards

Octopus agents use the JSON Schema defined in inputSchema to decide how to call your tool. A poorly defined schema produces hallucinated or missing arguments at runtime.

// Well-formed MCP tool definition
new MCPTool
{
    Name        = "crm_get_contact",
    Description = "Look up a CRM contact by email address. " +
                  "Returns contact name, account, phone, and open opportunities.",
    InputSchema = JsonDocument.Parse("""
    {
      "type": "object",
      "properties": {
        "email": {
          "type":        "string",
          "format":      "email",
          "description": "The contact's email address (required)"
        },
        "include_opportunities": {
          "type":        "boolean",
          "description": "Include open opportunities in the response",
          "default":     false
        }
      },
      "required": ["email"]
    }
    """)
}
Schema RuleReason
All required params listed in required arrayAgent knows which fields it must supply
Every property has a descriptionLLM uses descriptions to map user intent to schema
Use enum for fixed-value fieldsPrevents invalid values reaching your handler
Use format hints (email, date, uri)Agent validates before calling; better error messages
No additionalProperties: true without reasonAvoid accepting unknown fields that bypass validation
Tool name uses snake_case with plugin prefixAvoids collisions: crm_, email_, erp_

Principle 4: IOctopusPlugin Contract

The plugin lifecycle contract is strict. Violating it causes startup failures, resource leaks, or incorrect DI resolution.

public interface IOctopusPlugin
{
    // Called during DI container setup — register services here only.
    // Do NOT start connections, open sockets, or call external APIs.
    void OnRegister(IServiceCollection services, IOctopusConfig config);

    // Called after DI container is built and host is starting.
    // Resolve connections, start background workers, register MCP tools.
    Task OnStartAsync(IOctopusPluginContext context,
                      CancellationToken cancellationToken);

    // Called during graceful shutdown — release all resources.
    // Must complete within the host's shutdown timeout (default 30 s).
    Task OnStopAsync(CancellationToken cancellationToken);
}
ActionOnRegisterOnStartAsyncOnStopAsync
Register services in DIYesNoNo
Read configurationYes (for DI)Yes (for runtime)No
Open TCP connectionsNoYesClose them here
Register MCP toolsNoYesNo
Start background tasksNoYesCancel them here
Resolve credentialsNoYesNo

Principle 5: Graceful Degradation

External services fail. Network partitions happen. Rate limits are hit. A plugin that throws an unhandled exception in a tool handler will kill the agent's turn. Return structured errors instead.

public async Task<JsonElement> HandleGetContactAsync(JsonElement input)
{
    try
    {
        var contact = await _crm.GetContactAsync(
            email: input.GetProperty("email").GetString());

        return JsonSerializer.SerializeToElement(new
        {
            success = true,
            contact
        });
    }
    catch (CrmRateLimitException ex)
    {
        _logger.LogWarning(ex, "CRM rate limit hit for tenant {TenantId}",
                           _tenant.TenantId);
        return JsonSerializer.SerializeToElement(new
        {
            success      = false,
            error        = "rate_limit",
            message      = "CRM rate limit reached. Please retry in 60 seconds.",
            retry_after  = 60
        });
    }
    catch (CrmNotFoundException)
    {
        return JsonSerializer.SerializeToElement(new
        {
            success = false,
            error   = "not_found",
            message = $"No contact found for email: {input.GetProperty("email")}"
        });
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "Unexpected error in crm_get_contact");
        return JsonSerializer.SerializeToElement(new
        {
            success = false,
            error   = "internal_error",
            message = "An unexpected error occurred. Please try again."
        });
    }
}
Agent response quality. When your tool returns a structured error object, the LLM can read the message field and relay a meaningful explanation to the user — instead of a raw exception stack trace or a silent failure.

Plugin Compliance Checklist

#RequirementVerification
1All secrets via ICredentialResolverGrep for raw passwords / API keys in config
2All data reads scoped to ITenantContext.TenantIdCode review: every query/call checks tenant
3Every MCP tool has a valid JSON Schema with requiredCall GET /mcp/tools and validate schemas
4Every tool property has a descriptionReview InputSchema definitions
5OnRegister only registers DI — no I/OStep through startup; confirm no network calls
6OnStopAsync disposes all connections and cancels tasksTrigger graceful shutdown; confirm clean exit
7All tool handlers catch exceptions and return error JSONUnit test with simulated network failures
8Structured logging with tenant and correlation IDsReview log output under load
9Tool names prefixed with plugin namespace (crm_, email_)Check tool registry for naming collisions
10Rate-limit and retry logic for external APIsIntegration test against throttled mock