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:
- Authentication - Verify the caller’s identity via bearer tokens
- Scopes - Control which tools each token can access
- Rate Limiting - Prevent abuse with per-tool rate limits
- 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:
- Development mode (
micro run): Auth is disabled by default for easy development - Production mode (
micro server): JWT auth is enabled with user management at/auth
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:
- Gateway checks if the tool has required scopes
- If scopes are defined, the caller’s token must include at least one matching scope
- A token with scope
*has unrestricted access (admin) - If no scopes are defined for a tool, any authenticated token can call it
- 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/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:
| 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:
Connection-Level Auth (Recommended)
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:
- Auth enabled - Configure an
auth.Authprovider - Scopes defined - Every write/delete endpoint has required scopes
- Rate limits set - Appropriate limits for each service type
- Audit logging active - All calls logged to a persistent store
- HTTPS/TLS - MCP gateway behind TLS termination
- Token rotation - Process for rotating compromised tokens
- Monitoring - Alerts on high error rates or denied calls
- Testing - Verified scope enforcement with
micro mcp test
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
- Building AI-Native Services - End-to-end tutorial
- Tool Description Best Practices - Write effective documentation
- Agent Integration Patterns - Multi-agent architectures
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.
Feedback
Was this page helpful?
Glad to hear it! Please tell us how we can improve.
Sorry to hear that. Please tell us how we can improve.