-
Notifications
You must be signed in to change notification settings - Fork 4
Implement Go interceptor chain #4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Degiorgio
wants to merge
1
commit into
main
Choose a base branch
from
interceptors/go
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,8 @@ | |
| *.dll | ||
| *.so | ||
| *.dylib | ||
| mutator | ||
| validator | ||
|
|
||
| # Build results | ||
| [Bb]in/ | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,57 @@ | ||
| # MCP Interceptors - Go Implementation | ||
|
|
||
| This will contain the Go implementation of the MCP Interceptors based on [SEP-1763](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1763). | ||
| Go implementation of the MCP Interceptor Extension based on | ||
| [SEP-1763](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1763). | ||
|
|
||
| ## Quick Start | ||
|
|
||
| ```go | ||
| mcpServer := mcp.NewServer(&mcp.Implementation{ | ||
| Name: "my-server", | ||
| Version: "0.1.0", | ||
| }, nil) | ||
|
|
||
| // Wrap with interceptor support. | ||
| srv := interceptors.NewServer(mcpServer, | ||
| // Optional Context Provider | ||
| interceptors.WithContextProvider( | ||
| func(_ context.Context, _ mcp.Request) *interceptors.InvocationContext { | ||
| return &interceptors.InvocationContext{ | ||
| Principal: &interceptors.Principal{Type: "user", ID: "alice"}, | ||
| } | ||
| }, | ||
| ), | ||
| ) | ||
|
|
||
| // Register a validator that blocks dangerous tool calls. | ||
| srv.AddInterceptor(&interceptors.Validator{ | ||
| Metadata: interceptors.Metadata{ | ||
| Name: "block-dangerous", | ||
| Events: []string{interceptors.EventToolsCall}, | ||
| Phase: interceptors.PhaseRequest, | ||
| Mode: interceptors.ModeOn, | ||
| }, | ||
| Handler: func(_ context.Context, inv *interceptors.Invocation) (*interceptors.ValidationResult, error) { | ||
| // validate the request... | ||
| return &interceptors.ValidationResult{Valid: true}, nil | ||
| }, | ||
| }) | ||
|
|
||
| srv.Run(context.Background(), &mcp.StdioTransport{}) | ||
| ``` | ||
|
|
||
| See [`examples/`](examples/) for complete working examples. | ||
|
|
||
| ## Documentation | ||
|
|
||
| - [**DESIGN.md**](doc/DESIGN.md) — architecture, execution model, integration | ||
| with the go-sdk. | ||
| - [**PERFORMANCE.md**](doc/PERFORMANCE.md) — per-request cost model, allocation | ||
| summary, and optimization notes. | ||
| - [**CONFORMANCE.md**](doc/CONFORMANCE.md) — SEP conformance status. | ||
|
|
||
| Package API documentation is available via `go doc`: | ||
|
|
||
| ```sh | ||
| go doc github.com/modelcontextprotocol/ext-interceptors/go/sdk/interceptors | ||
| ``` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| # SEP Conformance | ||
|
|
||
| Status of this Go SDK implementation against the | ||
| [SEP-1763](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1763) | ||
| interceptor proposal. | ||
|
|
||
| ## Implemented | ||
|
|
||
| | Area | Notes | | ||
| |------|-------| | ||
| | Validation interceptors | Parallel execution, severity-based blocking, fail-open support | | ||
| | Mutation interceptors | Sequential execution, priority ordering, atomic payload updates | | ||
| | Interceptor metadata | Name, version, description, events, phase, priorityHint (polymorphic JSON), compat, configSchema, mode, failOpen, timeout | | ||
| | Event names | Constants for all standard server-side MCP methods; JSON-RPC method names used directly as event names | | ||
| | Unified result envelope | ValidationResult, MutationResult, ExecutionResult with base envelope fields | | ||
| | Chain result | ChainResult with status, results, finalPayload, validationSummary, abortedAt | | ||
| | JSON-RPC error mapping | Typed error data structs: -32602 for validation, -32603 for mutation, -32000 for timeout | | ||
| | Trust-boundary execution order | Receiving: validate (parallel) then mutate (sequential); Sending: mutate (sequential) then validate (parallel) | | ||
| | Priority ordering | Mutators sorted by `priorityHint.Resolve(phase)` ascending, alphabetical tiebreak | | ||
| | Fail-open behavior | `FailOpen: true` interceptors log errors without aborting the chain | | ||
| | Audit mode | `ModeAudit` records results without blocking or applying mutations | | ||
| | Timeout & context | Per-interceptor timeouts, chain-level context cancellation, `InvocationContext` with principal/traceId via `mcpserver.WithContextProvider` | | ||
| | Receiving direction (client → server) | All server-side method calls intercepted via `AddReceivingMiddleware` | | ||
| | Capability declaration | Interceptor metadata injected into `initialize` response via `Capabilities.Experimental` | | ||
| | First-party (in-process) deployment | Interceptors run as Go functions within the server process | | ||
| | Third-party and hybrid deployment | Handlers can call remote services; local and remote interceptors can be mixed freely. No built-in remote interceptor abstraction yet | | ||
|
|
||
| ## Not Implemented | ||
|
|
||
| | Area | SEP expects | Notes | | ||
| |------|-------------|-------| | ||
| | Wildcard event matching | `type InterceptorEvent = ... \| "*/request" \| "*/response" \| "*"` | `matchesEvent` does exact match only; wildcard patterns are planned | | ||
| | Protocol methods | `interceptors/list`, `interceptor/invoke`, `interceptor/executeChain` | Requires upstream go-sdk changes to register custom JSON-RPC methods | | ||
| | Server → client interception | Client features as interceptable events: `"sampling/createMessage"`, `"elicitation/create"`, `"roots/list"` | Requires a `sendingMiddleware` installed via `Server.AddSendingMiddleware`. Outgoing requests run mutate → validate, incoming responses run validate → mutate (same `executeForSending`/`executeForReceiving` methods). Interceptors match by event name (no API changes needed for registration) | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,195 @@ | ||
| # Go SDK Interceptors — Design Document | ||
|
|
||
| ## Integration Point: Receiving Middleware | ||
|
|
||
| The go-sdk processes an incoming JSON-RPC message in this order: | ||
|
|
||
| ``` | ||
| Transport (SSE / stdio) | ||
| → JSON-RPC decode | ||
| → Params deserialization (json.RawMessage → typed struct) | ||
| → Receiving middleware chain ← we hook in here | ||
| → Method handler (e.g. tool handler) | ||
| → Result returned through middleware | ||
| → JSON-RPC encode | ||
| → Transport | ||
| ``` | ||
|
|
||
| ## Capability Declaration | ||
|
|
||
| During initialization, the 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 | ||
| - `interceptors` — full metadata array in wire format | ||
|
|
||
| ## Request/Response Lifecycle | ||
|
|
||
| When a JSON-RPC request arrives, `receivingMiddleware` in | ||
| `mcpserver/server.go` runs the following sequence: | ||
|
|
||
| ``` | ||
| 0. If method == "initialize" → enrich result with capability declaration | ||
| 1. Assign typed params to Invocation inv.Payload = req.GetParams() | ||
| 2. Run request-phase chain (validate → mutate) | ||
| 3. If aborted → return error | ||
| 4. Params already modified in place — no unmarshal needed | ||
| 5. Call next handler next(ctx, method, req) | ||
| 6. Assign result to Invocation inv.Payload = result | ||
| 7. Run response-phase chain (mutate → validate) | ||
| 8. If aborted → return error | ||
| 9. Result already modified in place — no unmarshal needed | ||
| 10. Return result | ||
| ``` | ||
|
|
||
| The JSON-RPC method name is used directly as the event name (e.g. `"tools/call"`). | ||
|
|
||
| ### Typed Payload — Zero JSON Operations | ||
|
|
||
| `Invocation.Payload` is `any`, the live Go value from the go-sdk | ||
| (e.g. `*mcp.CallToolParamsRaw`). Handlers type-assert directly, the | ||
| same pattern as gRPC-Go interceptors (`req any`). No JSON marshaling | ||
| or unmarshaling occurs in the normal path. | ||
|
|
||
| ```go | ||
| // Validator — type-assert, inspect, return: | ||
| params, ok := inv.Payload.(*mcp.CallToolParamsRaw) | ||
| if !ok { | ||
| return nil, fmt.Errorf("unexpected payload type %T", inv.Payload) | ||
| } | ||
|
|
||
| // Mutator — type-assert, modify in place, return: | ||
| result, ok := inv.Payload.(*mcp.CallToolResult) | ||
| if !ok { | ||
| return nil, fmt.Errorf("unexpected payload type %T", inv.Payload) | ||
| } | ||
| result.Content[0] = &mcp.TextContent{Text: "modified"} | ||
| return &MutationResult{Modified: true}, nil | ||
| ``` | ||
|
|
||
| **Audit mode:** Audit-mode mutators receive a deep-copied payload (via | ||
| `Invocation.withCopiedPayload()`) so their in-place modifications don't | ||
| affect the real struct. The deep copy uses a JSON round-trip | ||
| (`json.Marshal` → `reflect.New` → `json.Unmarshal`). Only audit-mode | ||
| mutators pay this cost. | ||
|
|
||
| ### Limitations | ||
|
|
||
| - **Params must round-trip through JSON faithfully** for audit-mode deep | ||
| copy. All go-sdk param and result types use standard `encoding/json` | ||
| tags, so this holds in practice. | ||
| - **Type assertions require knowing the concrete type.** Interceptors must | ||
| know which type to expect for a given event (e.g. `*mcp.CallToolParamsRaw` | ||
| for `tools/call` requests). The `Events` field on `Metadata` narrows | ||
| which events reach a handler, so single-event interceptors always see | ||
| the expected type. | ||
|
|
||
| --- | ||
|
|
||
| ## 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 untouched. | ||
|
|
||
| ### Not Intercepted | ||
|
|
||
| 1. **Progress notifications.** During a tool call, a handler can call | ||
| `session.NotifyProgress()`. These are JSON-RPC *notifications* sent | ||
| directly over the transport — they do not flow through `MethodHandler` | ||
| middleware. Interceptors never see them. | ||
|
|
||
| 2. **Transport-level SSE streaming.** The Streamable HTTP transport | ||
| multiplexes multiple JSON-RPC messages over a single SSE connection. | ||
| This is connection management, not per-message streaming. Each individual | ||
| method call is still a single request → single response, which the | ||
| middleware intercepts normally. | ||
|
|
||
| 3. **JSON-RPC notifications** (e.g. `notifications/initialized`, | ||
| `notifications/cancelled`). The go-sdk routes notifications through a | ||
| separate handler path, not through `MethodHandler` middleware. | ||
|
|
||
| Notification interception is not defined by the proposal. | ||
| If this becomes necessary, it would require a separate notification middleware | ||
| hook in the go-sdk. | ||
|
|
||
| --- | ||
|
|
||
| ## Chain Execution Model | ||
|
|
||
| The `chainExecutor` in `chain_executor.go` implements trust-boundary-aware | ||
| execution: | ||
|
|
||
| **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. | ||
|
|
||
| ### Validator execution | ||
| - All matching validators run in parallel (goroutines). | ||
| - A validator returning `Valid: false` with `Severity: "error"` in enforced | ||
| mode (`Mode: ModeOn`) aborts the chain. | ||
| - `FailOpen: true` validators log errors and record an `ExecutionResult` | ||
| (with `Error` populated) for observability, but don't abort. | ||
|
|
||
| ### Mutator execution | ||
| - Mutators run sequentially, ordered by `PriorityHint.Resolve(phase)` | ||
| (ascending), with alphabetical name tiebreak. | ||
| - Each mutator modifies the typed payload in place via type assertion on `inv.Payload`. | ||
| - If any mutator fails (and is not `FailOpen`), the chain aborts. | ||
| `FailOpen` mutators record an `ExecutionResult` (with `Error` populated) | ||
| and continue. | ||
| - In `ModeAudit`, the mutator runs on a deep-copied payload and its result | ||
| is recorded, but the real payload is not affected. | ||
|
|
||
| ### Filtering | ||
| `newChainExecutor` filters the full interceptor set by: | ||
| 1. `Mode != ModeOff` | ||
| 2. Phase matches (or interceptor phase is `PhaseBoth`) | ||
| 3. Event matches (exact match only; wildcard support is planned via `matchesEvent`) | ||
|
|
||
| --- | ||
|
|
||
| ## File Map | ||
|
|
||
| ### `interceptors/` — protocol-agnostic core (zero MCP imports) | ||
|
|
||
| | File | Responsibility | | ||
| |------|---------------| | ||
| | `interceptor.go` | Types (Phase, Mode, InterceptorType, Priority, Severity, Compat), Metadata struct, Interceptor interface, Validator/Mutator structs and handler types | | ||
| | `invocation.go` | Invocation (with audit-mode payload cloning), InvocationContext, Principal — the input to every handler | | ||
| | `result.go` | All outcome types: ValidationResult, MutationResult, ExecutionResult, ChainResult, AbortInfo | | ||
| | `chain.go` | `Chain` public API: `NewChain`, `Add`, `ExecuteForReceiving`, `ExecuteForSending`, `IsEmpty`, `Interceptors` | | ||
| | `chain_executor.go` | `interceptorSnapshot` (atomic snapshot with lazy chain cache), `chainExecutor` struct, `newChainExecutor` (filtering + sorting), `executeForReceiving`, `executeForSending`, `timeoutResult`, `matchesPhase`, `matchesEvent` | | ||
| | `chain_validate.go` | `validatorResult` struct, `runValidators` (parallel dispatch + N=1 fast path), `executeValidator`, `recordValidation` | | ||
| | `chain_mutate.go` | `mutatorOutcome` type + constants, `runMutators` (sequential loop + audit-mode copy), `executeMutator` | | ||
|
|
||
| ### `interceptors/mcpserver/` — MCP server integration | ||
|
|
||
| | File | Responsibility | | ||
| |------|---------------| | ||
| | `server.go` | `Server` wrapper, `receivingMiddleware`, `WithContextProvider`, capability declaration, `NewStreamableHTTPHandler`, `abortToJSONRPCError` | | ||
| | `events.go` | Event name constants for standard MCP methods | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| # Go SDK Interceptors — Performance | ||
|
|
||
| Analysis of per-request costs and allocation patterns. | ||
|
|
||
| --- | ||
|
|
||
| ## Design Rationale: Typed Payload | ||
|
|
||
| The interceptor chain passes Go's typed params/result objects directly | ||
| to handlers, avoiding JSON serialization entirely in the normal path. | ||
| This follows the same pattern used by gRPC-Go, where interceptors | ||
| receive the request as `any` and type-assert to the concrete type. | ||
|
|
||
| `Invocation.Payload` is `any, the same pointer the go-sdk already | ||
| allocated during JSON-RPC deserialization. No wrapper struct, no | ||
| intermediate copies. Mutators modify the value in place through the | ||
| pointer (no marshal/unmarshal round-trip) | ||
|
|
||
| --- | ||
|
|
||
| ## Per-Request Cost Model | ||
|
|
||
| Every intercepted request passes through the middleware in | ||
| `mcpserver/server.go`. The cost depends on whether interceptors match | ||
| the event. | ||
|
|
||
| ### Fast Path (no matching interceptors) | ||
|
|
||
| When no interceptors match, the middleware cost is: | ||
|
|
||
| 1. One atomic pointer load: `s.chain.snapshot.Load()` | ||
| 2. Two `sync.Map` lookups on the chain cache: `getChain(event, PhaseRequest)` and `getChain(event, PhaseResponse)` | ||
| 3. One boolean check: `ce.empty` | ||
|
|
||
| Zero allocations, zero JSON operations. | ||
|
|
||
| ### Intercepted Path | ||
|
|
||
| With interceptors active, each active phase | ||
| incurs: | ||
|
|
||
| | Step | Operation | Allocations | JSON ops | | ||
| |------|-----------|-------------|----------| | ||
| | 1 | `Invocation` struct | 1 struct | 0 | | ||
| | 2 | `ChainResult` struct (with pre-allocated `Results` slice) | 1 struct + 1 slice | 0 | | ||
| | 3 | Validator execution | 0 (N=1) / goroutines (N>1) | 0 | | ||
| | 4 | Mutator execution | 0 | 0 | | ||
| | 5 | Audit-mode deep copy | 1 per audit mutator | 1 marshal + 1 unmarshal | | ||
|
|
||
| Zero JSON operations in the normal path. Phases with no matching | ||
| interceptors are skipped entirely. |
Empty file.
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you include go version 1.26 too
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, will add!