<!-- Provide a brief summary of your changes --> ## Motivation and Context The SEP-2624 spec defines interceptor modes as "active" | "audit". This change aligns the Go SDK's interceptor `Mode` value with the SEP spec. ## How Has This Been Tested? - go test ./... from go/sdk — all packages pass (chain, extension, integrations/gomiddleware). - gofmt -l . — clean. - go vet ./... — clean. ## Breaking Changes Yes: - The exported constant interceptors.ModeEnforce is removed and replaced by interceptors.ModeActive. Callers referencing ModeEnforce must rename. - The serialized mode value changes from "enforce" to "active". Any config expecting "enforce" must be updated. ## Types of changes <!-- What types of changes does your code introduce? Put an `x` in all the boxes that apply: --> - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [x] Breaking change (fix or feature that would cause existing functionality to change) - [x] Documentation update ## Checklist <!-- Go over all the following points, and put an `x` in all the boxes that apply. --> - [x] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [x] My code follows the repository's style guidelines - [x] New and existing tests pass locally - [ ] I have added appropriate error handling - [x] I have added or updated documentation as needed ## Additional context <!-- Add any other context, implementation notes, or design decisions --> Signed-off-by: monikaicampbell <mcampbell350@bloomberg.net>
8.9 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
- Registers the interceptor custom sending methods on the client
- 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.Extensions["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 or notification reaches the server receiving
middleware, gomiddleware.Middleware runs:
0. If method is skipped → 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. If the handler returns nil result (notification) → return
7. Marshal response result to json.RawMessage
8. chain.Execute(ctx, {event, PhaseResponse, payload})
→ For each interceptor: interceptor/invoke RPC via ClientSession
9. If aborted → return JSON-RPC error
10. Unmarshal mutated payload back into response result
11. 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) return the updated JSON in
MutationResult.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 messages routed through the server's receiving middleware except the skipped methods above. The current integration is server-side receiving middleware only.
| Method | Event |
|---|---|
tools/call |
EventToolsCall |
tools/list |
EventToolsList |
prompts/get |
EventPromptsGet |
prompts/list |
EventPromptsList |
resources/read |
EventResourcesRead |
resources/list |
EventResourcesList |
resources/subscribe |
EventResourcesSubscribe |
Custom methods registered with the Go SDK's receiving custom-method API also pass through the middleware and are intercepted using the JSON-RPC method name 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 active 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 returns updated JSON inMutationResult.Payload. The chain passes the returned 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 |
Interceptor JSON-RPC method names, public event constants for common server methods, 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 |
client.go |
RegisterSendingMethods, which registers interceptor custom methods on an MCP client before connecting |
rpc.go |
handleList and handleInvoke JSON-RPC method handlers |
interceptors/integrations/gomiddleware/ — middleware
| File | Responsibility |
|---|---|
middleware.go |
Middleware function, request/response interception, abort-to-JSON-RPC-error conversion |