## Summary - Rename `interceptors/mcpserver/` → `interceptors/extension/`, replacing the `Server` wrapper with a lightweight `Extension` that can install itself on any `*mcp.Server` - Remove the stored `*mcp.Server` field — `Extension` is now a pure interceptor container with no server reference - Add `Install(server)` method to register JSON-RPC methods and init middleware on a given server - Update `LocalChain(ctx)` → `LocalChain(ctx, server)` to accept the server as a parameter - Remove `MCPServer()`, `Run()`, and `NewStreamableHTTPHandler()` wrapper methods — users interact with `mcp.Server` directly - Update all examples, tests, and documentation to use the new API ## Why `mcpserver.Server` pretended to *be* the server — it had `Run()`, `NewStreamableHTTPHandler()`, etc. — but it could never cover the full `*mcp.Server` API, so users constantly needed `MCPServer()` to unwrap it. `Extension` doesn't pretend to be the server. It's a pure interceptor container that can install itself on any server (or multiple servers) on the fly. No stored server reference, no wrapping, no delegation surface to maintain. ### Before ```go mcpServer := mcp.NewServer(...) srv := mcpserver.NewServer(mcpServer) srv.AddInterceptor(v) chain, err := srv.LocalChain(ctx) ``` ### After ```go mcpServer := mcp.NewServer(...) ext := extension.New() ext.AddInterceptor(v) ext.Install(mcpServer) chain, err := ext.LocalChain(ctx, mcpServer) ``` Signed-off-by: Kurt Degiorgio <kdegiorgio@bloomberg.net>
8.5 KiB
Go SDK Interceptors — Design Document
Architecture: SEP-Compliant Chain with interceptor/invoke
The Go SDK follows the SEP execution model where interceptors are first-class MCP primitives:
- Interceptor Extension (
extension.Extension) — registers interceptors and installs them on one or moremcp.Serverinstances, exposing them viainterceptors/listandinterceptor/invokeJSON-RPC methods. - Chain (
chain.Chain) — the SDK-level orchestrator in theinterceptors/chainsub-package. It holdsChainEntryobjects (interceptor descriptor + MCP client session) and invokes interceptors viainterceptor/invokeon the appropriate server. - Middleware (
gomiddleware.Middleware) — hooks into the go-sdk's receiving middleware pipeline, marshals payloads tojson.RawMessage, callschain.Execute(), and applies mutated payloads back.
Transport (SSE / stdio / HTTP)
→ JSON-RPC decode
→ Params deserialization (json.RawMessage → typed struct)
→ Receiving middleware chain ← gomiddleware hooks in here
→ chain.Execute() → interceptor/invoke RPC (per entry)
→ Method handler (e.g. tool handler)
→ Result returned through middleware
→ chain.Execute() → interceptor/invoke RPC (per entry)
→ JSON-RPC encode
→ Transport
LocalChain: In-Memory Transport
Extension.LocalChain(ctx, server) creates a chain connected to the given
server via in-memory transport. Under the hood it:
- Creates an
InMemoryTransportpair viamcp.NewInMemoryTransports() - Connects the server side via
mcp.Server.Connect(ctx, serverTransport) - Creates an MCP client and connects via the client transport
- Calls
chain.AddMCPServer(ctx, clientSession)which discovers interceptors viainterceptors/list - Returns the ready-to-use chain
This means interceptors are invoked through the full MCP JSON-RPC pathway — even when running in the same process — ensuring the same behavior as remote interceptor servers.
Capability Declaration
During initialization, a lightweight middleware intercepts the "initialize"
response and injects interceptor metadata into
Capabilities.Experimental["io.modelcontextprotocol/interceptors"]. This
follows the same pattern as the variants extension
(io.modelcontextprotocol/server-variants).
The capability payload includes:
supportedEvents— deduplicated list of events with registered interceptors
Request/Response Lifecycle
When a JSON-RPC request arrives, gomiddleware.Middleware runs:
0. If method is skipped (initialize, notifications/*, interceptor/*) → passthrough
1. Marshal request params to json.RawMessage
2. chain.Execute(ctx, {event, PhaseRequest, payload})
→ For each interceptor: interceptor/invoke RPC via ClientSession
3. If aborted → return JSON-RPC error
4. Unmarshal mutated payload back into request params
5. Call next handler next(ctx, method, req)
6. Marshal response result to json.RawMessage
7. chain.Execute(ctx, {event, PhaseResponse, payload})
→ For each interceptor: interceptor/invoke RPC via ClientSession
8. If aborted → return JSON-RPC error
9. Unmarshal mutated payload back into response result
10. Return result
JSON-RPC Payload Model
Interceptor handlers receive json.RawMessage as inv.Payload when
invoked via interceptor/invoke. This is the SEP-correct behavior —
payloads are JSON at the protocol level. Handlers unmarshal, inspect or
modify, and (for mutators) set the updated JSON back on inv.Payload:
// Validator — unmarshal, inspect, return:
raw := inv.Payload.(json.RawMessage)
var params struct{ Name string `json:"name"` }
json.Unmarshal(raw, ¶ms)
// inspect params ...
return &ValidationResult{Valid: true}, nil
// Mutator — unmarshal, modify, marshal back:
raw := inv.Payload.(json.RawMessage)
var result map[string]any
json.Unmarshal(raw, &result)
result["redacted"] = true
data, _ := json.Marshal(result)
return &MutationResult{Modified: true, Payload: data}, nil
Skipped Methods
The middleware skips the following methods to prevent recursion and avoid intercepting lifecycle events:
initialize— capability enrichment handled separatelynotifications/initialized— lifecycle notificationnotifications/cancelled— lifecycle notificationinterceptors/list— interceptor discovery (would cause recursion)interceptor/invoke— interceptor invocation (would cause recursion)
What Is and Is Not Intercepted
Intercepted
All JSON-RPC method calls routed through the server's receiving middleware:
| Method | Event |
|---|---|
tools/call |
EventToolsCall |
tools/list |
EventToolsList |
prompts/get |
EventPromptsGet |
prompts/list |
EventPromptsList |
resources/read |
EventResourcesRead |
resources/list |
EventResourcesList |
resources/subscribe |
EventResourcesSubscribe |
Unknown methods pass through the middleware and are intercepted normally (the JSON-RPC method name is used as the event name).
Not Intercepted
-
Progress notifications. These are JSON-RPC notifications sent directly over the transport — they do not flow through
MethodHandlermiddleware. -
Transport-level SSE streaming. Connection management, not per-message streaming. Each individual method call is still a single request → single response.
-
JSON-RPC notifications (e.g.
notifications/initialized,notifications/cancelled). Explicitly skipped by the middleware.
Chain Execution Model
Chain.Execute() implements trust-boundary-aware execution per the SEP:
Request phase (receiving data — untrusted → trusted):
Validate (parallel) → Mutate (sequential)
Validation acts as a security gate before mutations process the data.
Response phase (sending data — trusted → untrusted):
Mutate (sequential) → Validate (parallel)
Mutations prepare/sanitize data, then validation verifies before sending. Response-phase validators see the post-mutation payload.
Validator execution
- All matching validators run in parallel (goroutines).
- A validator returning
Valid: falsewithSeverity: "error"in enforced mode aborts the chain. FailOpen: truevalidators log errors and record anInvokeResultfor observability, but don't abort.
Mutator execution
- Mutators run sequentially, ordered by
PriorityHint.Resolve(phase)(ascending), with alphabetical name tiebreak. - Each mutator receives
json.RawMessage, unmarshals, modifies, and sets the updated JSON back oninv.Payload. The chain passes the mutated payload from each mutator to the next. - If any mutator fails (and is not
FailOpen), the chain aborts. - In
ModeAudit, the mutated payload is not propagated to subsequent interceptors.
Filtering
Chain.Execute filters entries by:
- Phase matches (or interceptor phase is
PhaseBoth) - Event matches (exact match only; wildcard support is planned)
- Optional name filter (
ChainExecutionParams.Interceptors)
File Map
interceptors/ — core types (transport-agnostic)
| File | Responsibility |
|---|---|
interceptor.go |
Interceptor interface, enums (Phase, Mode, InterceptorType, Severity), Metadata/Compat/Hook, Validator/Mutator structs and handler types, Invocation/InvocationContext/Principal, result types (ValidationResult, MutationResult) |
priority.go |
Priority struct, NewPriority, Resolve, MarshalJSON, UnmarshalJSON |
wire.go |
JSON-RPC method/event constants, wire types (ListParams, ListResult, InvokeParams, InvokeResult, InterceptorInfo) |
interceptors/chain/ — chain orchestrator
| File | Responsibility |
|---|---|
chain.go |
Chain, ChainEntry, ExecutionParams, ExecutionResult, NewChain, AddMCPServer, Execute; filtering, sorting, parallel validation, sequential mutation, trust-boundary ordering |
result.go |
Chain result types: ChainStatus, AbortType, AbortInfo, ValidationSummary |
interceptors/extension/ — MCP server integration
| File | Responsibility |
|---|---|
server.go |
Extension, interceptor management, Install, LocalChain, capability declaration |
rpc.go |
handleList and handleInvoke JSON-RPC method handlers |
events.go |
Event name constants for standard MCP methods |
interceptors/integrations/gomiddleware/ — middleware
| File | Responsibility |
|---|---|
middleware.go |
Middleware function, request/response interception, abort-to-JSON-RPC-error conversion |