Portal Community

Authentication Flow

  1. Operator stores a shared secret (API key or JWT) in the Octopus credential store via the Credentials API
  2. The resulting credentialId integer is set in the agent's mcpServers[].credentialId field
  3. At agent initialization, Octopus calls ICredentialResolver.GetPasswordAsync(credentialId)
  4. The resolved secret is sent as Authorization: Bearer {secret} on all requests to the MCP server
  5. Your MCP server validates the token on every incoming request
// Octopus — how the token is resolved and sent (internal)
var token = await _credentialResolver.GetPasswordAsync(server.CredentialId);
var request = new HttpRequestMessage(HttpMethod.Get, $"{server.BaseUrl}/tools");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
var response = await _httpClient.SendAsync(request);

Storing the Credential

// POST /api/credentials — store the MCP server's shared secret
{
  "name":        "Zendesk MCP Server Token",
  "description": "Bearer token for the Zendesk MCP tool server",
  "type":        "ApiKey",
  "value":       "sk-zendesk-mcp-prod-1234567890abcdef"
}

// Response
{
  "credentialId": 55,   // use this in mcpServers[].credentialId
  "name":        "Zendesk MCP Server Token"
}
Never put the token in appsettings. The credential value must be stored in the Octopus credential store — not in appsettings.json, environment variables, or source control. The credential store encrypts values at rest.

Validating the Token on Your MCP Server (Static API Key)

For servers that use a static API key (not JWT), validate the token using a simple middleware or policy:

// C# MinimalAPI — static API key validation middleware
app.Use(async (context, next) =>
{
    if (context.Request.Path == "/health")
    {
        await next(context);
        return;
    }

    var authHeader = context.Request.Headers["Authorization"].FirstOrDefault();
    if (authHeader is null || !authHeader.StartsWith("Bearer "))
    {
        context.Response.StatusCode = 401;
        await context.Response.WriteAsJsonAsync(new
        {
            error   = "unauthorized",
            message = "Missing or invalid Authorization header."
        });
        return;
    }

    var providedToken = authHeader["Bearer ".Length..].Trim();
    var expectedToken = context.RequestServices
                               .GetRequiredService<IConfiguration>()["McpServer:ApiKey"];

    if (!CryptographicOperations.FixedTimeEquals(
            Encoding.UTF8.GetBytes(providedToken),
            Encoding.UTF8.GetBytes(expectedToken ?? "")))
    {
        context.Response.StatusCode = 401;
        await context.Response.WriteAsJsonAsync(new
        {
            error   = "unauthorized",
            message = "Invalid token."
        });
        return;
    }

    await next(context);
});

Validating the Token on Your MCP Server (JWT)

If your MCP server is registered as an Azure AD application (recommended for enterprise deployments), validate the JWT using the standard AddJwtBearer middleware:

// Program.cs
builder.Services.AddAuthentication("Bearer")
    .AddJwtBearer("Bearer", options =>
    {
        // Azure AD — validates iss, aud, exp, sig
        options.Authority = "https://login.microsoftonline.com/{tenantId}/v2.0";
        options.Audience  = "api://my-mcp-server";

        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer           = true,
            ValidateAudience         = true,
            ValidateLifetime         = true,
            ValidateIssuerSigningKey = true
        };
    });

builder.Services.AddAuthorization(options =>
{
    // Optional: require a specific scope on tool calls
    options.AddPolicy("McpToolCaller", policy =>
        policy.RequireClaim("scp", "MCP.Call"));
});

// Map endpoints with RequireAuthorization
app.MapGet("/tools", ...)
   .RequireAuthorization("McpToolCaller");

app.MapPost("/tool/{name}/call", ...)
   .RequireAuthorization("McpToolCaller");

Authentication Options Comparison

OptionComplexityToken RotationUse Case
Static API keyLowManual (update credential store)Internal / single-operator deployments
Azure AD JWT (app-to-app)MediumAutomatic (1-hour JWT expiry)Enterprise / multi-tenant deployments
mTLS (mutual TLS)HighCertificate rotationHigh-security / zero-trust environments

Rotating Credentials

When you rotate the API key or token on your MCP server, update the Octopus credential store with the new value. Octopus re-reads credentials on each tool-discovery cycle — no agent restart is required for the new token to take effect.

// PATCH /api/credentials/{credentialId}
{
  "value": "sk-zendesk-mcp-prod-NEW-key-abcdef1234"
}

Per-Tenant Token Support

If your MCP server manages multiple tenants and each has its own API key, store a separate credential per tenant and register different MCP server entries per agent:

// Agent for tenant A
{
  "mcpServers": [
    {
      "name":         "zendesk",
      "baseUrl":      "https://mcp-zendesk.internal",
      "credentialId": 55   // tenant A's Zendesk token
    }
  ]
}

// Agent for tenant B
{
  "mcpServers": [
    {
      "name":         "zendesk",
      "baseUrl":      "https://mcp-zendesk.internal",
      "credentialId": 56   // tenant B's Zendesk token
    }
  ]
}

On your MCP server, read the X-Octopus-Tenant-Id header to scope the Zendesk API calls to the correct tenant — and validate that the Bearer token is authorised for that tenant ID.