Middleware
Middleware lets you wrap tool handlers with cross-cutting logic such as logging, authentication, and rate limiting. Middlewares execute in registration order (first registered runs first) and form a chain around the underlying tool handler.
Overview
A ToolHandlerMiddleware is a function that takes a ToolHandlerFunc and returns a new ToolHandlerFunc. This pattern is identical to HTTP middleware in Go's standard library.
type ToolHandlerFunc func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error)
type ToolHandlerMiddleware func(ToolHandlerFunc) ToolHandlerFuncRegistering Middleware
Using Use() (Runtime)
The Use() method registers one or more tool handler middlewares on a running server. Middlewares can be added at any time — Use() is concurrency-safe.
s := server.NewMCPServer("my-server", "1.0.0")
// Register a single middleware
s.Use(loggingMiddleware)
// Register multiple middlewares at once
s.Use(authMiddleware, rateLimitMiddleware)Middlewares registered via Use() execute in the order they were added. In the example above, a tool call passes through loggingMiddleware → authMiddleware → rateLimitMiddleware → tool handler.
Using Server Options (Construction Time)
You can also register middleware when constructing the server:
s := server.NewMCPServer("my-server", "1.0.0",
server.WithToolHandlerMiddleware(loggingMiddleware),
server.WithResourceHandlerMiddleware(resourceLogger),
server.WithPromptHandlerMiddleware(promptLogger),
)WithToolHandlerMiddleware, WithResourceHandlerMiddleware, and WithPromptHandlerMiddleware register middleware for their respective handler types at construction time.
Writing Middleware
Simple Middleware
A basic middleware that logs every tool call:
import (
"context"
"log"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
func loggingMiddleware(next server.ToolHandlerFunc) server.ToolHandlerFunc {
return func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Printf("tool call: %s", req.Params.Name)
return next(ctx, req)
}
}
s := server.NewMCPServer("my-server", "1.0.0")
s.Use(loggingMiddleware)Timing Middleware
Measure how long each tool call takes:
func timingMiddleware(next server.ToolHandlerFunc) server.ToolHandlerFunc {
return func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
start := time.Now()
result, err := next(ctx, req)
log.Printf("tool=%s duration=%v", req.Params.Name, time.Since(start))
return result, err
}
}Authentication Middleware
Reject unauthenticated requests before they reach the tool handler. You need to provide your own extractToken, validateToken, and userKey implementations:
// userKey is a custom context key for storing the authenticated user.
type contextKey string
const userKey contextKey = "user"
func authMiddleware(next server.ToolHandlerFunc) server.ToolHandlerFunc {
return func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
token := extractToken(ctx) // your token extraction logic
if token == "" {
return nil, fmt.Errorf("authentication required")
}
user, err := validateToken(token) // your token validation logic
if err != nil {
return nil, fmt.Errorf("invalid token: %w", err)
}
ctx = context.WithValue(ctx, userKey, user)
return next(ctx, req)
}
}Rate-Limiting Middleware
Limit the number of tool calls per session. This example uses golang.org/x/time/rate and server.GetSessionID() to identify the caller:
type rateLimiter struct {
limiters sync.Map
rate rate.Limit
burst int
}
func newRateLimiter(rps float64, burst int) *rateLimiter {
return &rateLimiter{rate: rate.Limit(rps), burst: burst}
}
func (r *rateLimiter) middleware(next server.ToolHandlerFunc) server.ToolHandlerFunc {
return func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
sessionID := server.GetSessionID(ctx)
limiter, _ := r.limiters.LoadOrStore(sessionID, rate.NewLimiter(r.rate, r.burst))
if !limiter.(*rate.Limiter).Allow() {
return nil, fmt.Errorf("rate limit exceeded")
}
return next(ctx, req)
}
}
// Usage
rl := newRateLimiter(10, 20)
s.Use(rl.middleware)Execution Order
Middlewares execute in registration order — the first middleware registered is the outermost wrapper and runs first:
s.Use(A, B)
s.Use(C)
// Call order: A → B → C → tool handler → C → B → AThis matches the convention used by net/http middleware in Go.
Task-Augmented Tools
Middleware registered via Use() also applies to regular tools executed via the task path (when a tool has TaskSupportOptional or TaskSupportPreferred and the client requests task execution). The same middleware chain wraps the regular tool handler automatically.
Note: Native task tools (those registered with a TaskToolHandlerFunc) do not have Use() middleware applied. If you need cross-cutting logic for native task tools, implement it directly in your task handler.
tool := mcp.NewTool("long-running",
mcp.WithDescription("A tool that supports task mode"),
mcp.WithTaskSupport(mcp.TaskSupportOptional),
)
s.AddTool(tool, handler)
// This middleware applies to regular calls and when this regular tool is executed via the task path
s.Use(loggingMiddleware)Built-in Middleware
MCP-Go includes built-in recovery middleware that catches panics in tool handlers and returns an error response instead of crashing the server:
s := server.NewMCPServer("my-server", "1.0.0",
server.WithRecovery(),
)Concurrency Safety
Use() is safe to call from multiple goroutines concurrently. Internally, it uses a read-write mutex to protect the middleware slice. This means you can dynamically add middleware at runtime without stopping the server.
Next Steps
- Tools — Define tools that middleware wraps
- Task-Augmented Tools — Long-running tools with task support
- Advanced Features — Hooks, session management, and more
