experimental-ext-interceptors/go/sdk/doc/DESIGN.md
monikaicampbell bd57572579
Go SDK: Align mode with SEP (#40)
<!-- 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>
2026-08-21 15:56:07 +01:00

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:

  1. Interceptor Extension (extension.Extension) — registers interceptors and installs them on one or more mcp.Server instances, exposing them via interceptors/list and interceptor/invoke JSON-RPC methods.
  2. Chain (chain.Chain) — the SDK-level orchestrator in the interceptors/chain sub-package. It holds ChainEntry objects (interceptor descriptor + MCP client session) and invokes interceptors via interceptor/invoke on the appropriate server.
  3. Middleware (gomiddleware.Middleware) — hooks into the go-sdk's receiving middleware pipeline, marshals payloads to json.RawMessage, calls chain.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:

  1. Creates an InMemoryTransport pair via mcp.NewInMemoryTransports()
  2. Connects the server side via mcp.Server.Connect(ctx, serverTransport)
  3. Creates an MCP client and connects via the client transport
  4. Registers the interceptor custom sending methods on the client
  5. Calls chain.AddMCPServer(ctx, clientSession) which discovers interceptors via interceptors/list
  6. 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, &params)
// 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 separately
  • notifications/initialized — lifecycle notification
  • notifications/cancelled — lifecycle notification
  • interceptors/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

  1. Progress notifications. These are JSON-RPC notifications sent directly over the transport — they do not flow through MethodHandler middleware.

  2. Transport-level SSE streaming. Connection management, not per-message streaming. Each individual method call is still a single request → single response.

  3. 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: false with Severity: "error" in active mode aborts the chain.
  • FailOpen: true validators log errors and record an InvokeResult for 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 in MutationResult.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:

  1. Phase matches (or interceptor phase is PhaseBoth)
  2. Event matches (exact match only; wildcard support is planned)
  3. 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