Older documentation. Start with the current guides.

MCP Security Guide

This guide covers how to secure your MCP gateway for production use, including authentication, per-tool scopes, rate limiting, and audit logging.

Overview

The MCP gateway provides four layers of security:

  1. Authentication - Verify the caller’s identity via bearer tokens
  2. Scopes - Control which tools each token can access
  3. Rate Limiting - Prevent abuse with per-tool rate limits
  4. Audit Logging - Record every tool call for compliance and debugging

Authentication

Bearer Token Auth

The MCP gateway uses bearer token authentication. Tokens are validated by the configured auth.Auth provider.

import (
    "go-micro.dev/v6/gateway/mcp"
    "go-micro.dev/v6/auth"
)

gateway := mcp.ListenAndServe(":3000", mcp.Options{
    Registry: service.Options().Registry,
    Auth:     authProvider, // auth.Auth implementation
})

Agents pass tokens in the Authorization header:

curl -X POST http://localhost:3000/mcp/call \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{"tool": "tasks.TaskService.Create", "input": {"title": "New task"}}'

Using micro run / micro server

When using micro run or micro server, authentication is handled automatically:

Create tokens with specific scopes via the dashboard at /auth/tokens.

Per-Tool Scopes

Scopes control which tools a token can access. There are two ways to set scopes.

Service-Level Scopes

Set scopes when registering your handler. These travel with the service through the registry:

handler := service.Server().NewHandler(
    new(TaskService),
    server.WithEndpointScopes("TaskService.Get", "tasks:read"),
    server.WithEndpointScopes("TaskService.List", "tasks:read"),
    server.WithEndpointScopes("TaskService.Create", "tasks:write"),
    server.WithEndpointScopes("TaskService.Update", "tasks:write"),
    server.WithEndpointScopes("TaskService.Delete", "tasks:admin"),
)

Gateway-Level Scopes

Override or add scopes at the gateway without modifying services. Gateway scopes take precedence:

mcp.ListenAndServe(":3000", mcp.Options{
    Registry: reg,
    Auth:     authProvider,
    Scopes: map[string][]string{
        "tasks.TaskService.Create": {"tasks:write"},
        "tasks.TaskService.Delete": {"tasks:admin"},
        "billing.Billing.Charge":   {"billing:admin"},
    },
})

Scope Enforcement

When a tool is called:

  1. Gateway checks if the tool has required scopes
  2. If scopes are defined, the caller’s token must include at least one matching scope
  3. A token with scope * has unrestricted access (admin)
  4. If no scopes are defined for a tool, any authenticated token can call it
  5. Denied calls return 403 Forbidden

Common Scope Patterns

PatternUse Case
service:readRead-only access to a service
service:writeCreate and update operations
service:adminDelete and destructive operations
*Full admin access (use sparingly)
internalInternal-only tools not exposed to external agents

Token Examples

Token A: scopes=["tasks:read"]
  ✅ Can call TaskService.Get, TaskService.List
  ❌ Cannot call TaskService.Create, TaskService.Delete

Token B: scopes=["tasks:read", "tasks:write"]
  ✅ Can call Get, List, Create, Update
  ❌ Cannot call TaskService.Delete (needs tasks:admin)

Token C: scopes=["*"]
  ✅ Can call everything (admin)

Rate Limiting

Prevent abuse with per-tool rate limiting using a token bucket algorithm:

mcp.ListenAndServe(":3000", mcp.Options{
    Registry: reg,
    RateLimit: &mcp.RateLimitConfig{
        RequestsPerSecond: 10,  // Sustained rate
        Burst:             20,  // Allow bursts up to 20
    },
})

When the rate limit is exceeded, calls return 429 Too Many Requests.

Choosing Rate Limits

Service TypeRequests/secBurstRationale
Read-heavy API50100High throughput, low cost
Write API1020Moderate, prevents spam
Expensive operation25Protect downstream resources
Internal tool100200Trusted callers, higher limits

Audit Logging

Record every tool call for compliance, debugging, and analytics:

mcp.ListenAndServe(":3000", mcp.Options{
    Registry: reg,
    Auth:     authProvider,
    AuditFunc: func(record mcp.AuditRecord) {
        log.Printf("[AUDIT] tool=%s account=%s allowed=%v duration=%v err=%v",
            record.Tool,
            record.AccountID,
            record.Allowed,
            record.Duration,
            record.Error,
        )
    },
})

AuditRecord Fields

FieldTypeDescription
ToolstringFull tool name (e.g., tasks.TaskService.Create)
AccountIDstringCaller’s account ID from the auth token
Scopes[]stringScopes on the caller’s token
AllowedboolWhether the call was permitted
Durationtime.DurationHow long the call took
ErrorerrorError if the call failed
TraceIDstringUUID trace ID for correlation
DeniedReasonstringWhy the call was denied (empty if allowed)

Production Audit Logging

For production, send audit records to a structured logging system:

AuditFunc: func(r mcp.AuditRecord) {
    // Structured JSON logging
    logger.Info("mcp_tool_call",
        "tool", r.Tool,
        "account", r.AccountID,
        "allowed", r.Allowed,
        "duration_ms", r.Duration.Milliseconds(),
        "trace_id", r.TraceID,
    )

    // Alert on denied calls
    if !r.Allowed {
        alerting.Notify("MCP access denied",
            "tool", r.Tool,
            "account", r.AccountID,
        )
    }
},

Tracing

Every MCP tool call gets a UUID trace ID, propagated via metadata headers:

HeaderDescription
Mcp-Trace-IdUUID for the tool call
Mcp-Tool-NameName of the tool called
Mcp-Account-IdCaller’s account ID

These are available in your handler via context metadata:

func (t *TaskService) Create(ctx context.Context, req *CreateRequest, rsp *CreateResponse) error {
    md, _ := metadata.FromContext(ctx)
    traceID := md["Mcp-Trace-Id"]
    log.Printf("Creating task, trace: %s", traceID)
    // ...
}

OpenTelemetry Integration

For full distributed tracing, plug in an OpenTelemetry trace provider:

import (
    "go.opentelemetry.io/otel"
    "go-micro.dev/v6/gateway/mcp"
)

mcp.ListenAndServe(":3000", mcp.Options{
    Registry:      reg,
    TraceProvider: otel.GetTracerProvider(),
})

Each tool call creates a span (mcp.tool.call) with these attributes:

AttributeExample
mcp.tool.nametasks.TaskService.Create
mcp.transporthttp, websocket, stdio
mcp.account.iduser-123
mcp.trace.ida1b2c3d4-...
mcp.auth.allowedtrue
mcp.auth.denied_reasoninsufficient_scope
mcp.scopes.requiredtasks:write
mcp.rate_limitedfalse

The gateway propagates W3C trace context downstream, so you get end-to-end traces from agent → gateway → service in Jaeger, Zipkin, or any OTel-compatible backend.

WebSocket Authentication

The WebSocket transport supports two authentication methods:

Pass the token in the WebSocket upgrade request:

const ws = new WebSocket("ws://localhost:3000/mcp/ws", {
  headers: { "Authorization": "Bearer <token>" }
});

The token is validated once on connection and applies to all messages on that connection.

Per-Message Auth

For stateless connections, pass a _token parameter with each tool call:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "tasks.TaskService.Create",
    "arguments": {"title": "New task"},
    "_token": "Bearer <token>"
  }
}

Connection-level auth takes precedence over per-message auth.

Production Checklist

Before deploying MCP to production:

Full Example

package main

import (
    "log"

    "go-micro.dev/v6"
    "go-micro.dev/v6/auth"
    "go-micro.dev/v6/gateway/mcp"
    "go-micro.dev/v6/server"
)

func main() {
    service := micro.NewService("tasks",
        micro.Address(":8081"),
    )
    service.Init()

    // Register handler with scopes
    handler := service.Server().NewHandler(
        &TaskService{tasks: make(map[string]*Task)},
        server.WithEndpointScopes("TaskService.Get", "tasks:read"),
        server.WithEndpointScopes("TaskService.Create", "tasks:write"),
        server.WithEndpointScopes("TaskService.Delete", "tasks:admin"),
    )
    service.Server().Handle(handler)

    // Start MCP gateway with full security
    go mcp.ListenAndServe(":3000", mcp.Options{
        Registry: service.Options().Registry,
        Auth:     service.Options().Auth,
        Scopes: map[string][]string{
            // Gateway-level overrides
            "billing.Billing.Charge": {"billing:admin"},
        },
        RateLimit: &mcp.RateLimitConfig{
            RequestsPerSecond: 10,
            Burst:             20,
        },
        AuditFunc: func(r mcp.AuditRecord) {
            log.Printf("[AUDIT] tool=%s account=%s allowed=%v duration=%v",
                r.Tool, r.AccountID, r.Allowed, r.Duration)
        },
    })

    service.Run()
}

Next Steps

Browser origins

HTTP and WebSocket MCP transports now reject cross-origin browser requests by default. Native clients that omit Origin continue to work. To allow a trusted browser application, set exact origins in mcp.Options.AllowedOrigins, or pass mcp.WithAllowedOrigins("https://console.example.com") to mcp.NewHandler. Wildcards are not accepted. Origin permission does not replace authentication. For micro mcp serve, micro gateway, and micro run, use the repeatable --mcp-allowed-origins https://console.example.com flag or the comma-separated MICRO_MCP_ALLOWED_ORIGINS environment variable.

Reverse proxies should preserve the original Host. When TLS terminates at a proxy, explicitly allow the public HTTPS origin in the MCP options. Do not trust arbitrary client-supplied forwarded headers to establish an allowed origin. For loopback sockets, a browser-controlled hostname is not sufficient for origin authorization; non-loopback hostnames require an explicit trusted origin.

The gateway dashboard’s user, token and scope administration pages require an administrator account or the configured machine token. Tool invocation scopes, including *, do not grant dashboard administration privileges.

Streamable HTTP requests and WebSocket upgrades also enforce the configured AuthFunc. Bearer credentials are inspected on each tool call so an existing connection cannot retain a revoked or expired authorization decision.

Paid tools must use the HTTP MCP endpoint for x402 verification and settlement; WebSocket calls to priced tools are rejected. Free WebSocket tools are unchanged.