Go Micro Logo Go Micro

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/v5/gateway/mcp"
    "go-micro.dev/v5/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

Pattern Use Case
service:read Read-only access to a service
service:write Create and update operations
service:admin Delete and destructive operations
* Full admin access (use sparingly)
internal Internal-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 Type Requests/sec Burst Rationale
Read-heavy API 50 100 High throughput, low cost
Write API 10 20 Moderate, prevents spam
Expensive operation 2 5 Protect downstream resources
Internal tool 100 200 Trusted 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

Field Type Description
Tool string Full tool name (e.g., tasks.TaskService.Create)
AccountID string Caller’s account ID from the auth token
Scopes []string Scopes on the caller’s token
Allowed bool Whether the call was permitted
Duration time.Duration How long the call took
Error error Error if the call failed
TraceID string UUID trace ID for correlation
DeniedReason string Why 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:

Header Description
Mcp-Trace-Id UUID for the tool call
Mcp-Tool-Name Name of the tool called
Mcp-Account-Id Caller’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/v5/gateway/mcp"
)

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

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

Attribute Example
mcp.tool.name tasks.TaskService.Create
mcp.transport http, websocket, stdio
mcp.account.id user-123
mcp.trace.id a1b2c3d4-...
mcp.auth.allowed true
mcp.auth.denied_reason insufficient_scope
mcp.scopes.required tasks:write
mcp.rate_limited false

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/v5"
    "go-micro.dev/v5/auth"
    "go-micro.dev/v5/gateway/mcp"
    "go-micro.dev/v5/server"
)

func main() {
    service := micro.NewService(
        micro.Name("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