experimental-ext-interceptors/go/sdk/doc/PERFORMANCE.md
Kurt Degiorgio c9900f02ab
Implement Go interceptor chain (#4)
## Summary

Implements the Go SDK for MCP interceptors — validation and mutation
middleware for any MCP server.

The framework treats interceptors as first-class MCP primitives: they
are registered on an MCP server, discoverable via `interceptors/list`,
and invocable via `interceptor/invoke` — the same JSON-RPC pattern as
tools, prompts, and resources. A chain engine orchestrates execution
across one or more interceptor servers, and an optional go-sdk
middleware integration applies interceptors automatically on every
matching request/response.

## Architecture

**Three-layer design:**

1. **`interceptors`** — Protocol-agnostic core. Defines interceptor
types (validators, mutators), metadata, invocation, wire types, and
results. Zero MCP dependencies.

2. **`interceptors/mcpserver`** — MCP server integration. Wraps
`mcp.Server` to register `interceptors/list` and `interceptor/invoke` as
custom JSON-RPC methods and advertise interceptor capabilities during
`initialize`.

3. **`interceptors/chain`** — Chain engine. Orchestrates execution
across N interceptor servers with trust-boundary-aware ordering,
priority-based mutation sequencing, parallel validation, audit-mode
isolation, and fail-open/fail-closed semantics.

4. **`interceptors/integrations/gomiddleware`** — Optional go-sdk
middleware. Installs on `mcp.Server` via `AddReceivingMiddleware()` to
automatically run the interceptor chain on every matching
request/response.

**Key design decisions:**

- **Trust-boundary ordering** — request phase: validate → mutate;
response phase: mutate → validate. Validation always guards the boundary
crossing.
- **Wire-level invocation** — the chain calls `interceptor/invoke` over
the MCP transport (even in-process via `LocalChain()`), so local and
remote interceptor servers use the same interface.
- **Audit-mode isolation** — audit-mode mutators compute transformations
without applying them. Only audit mutators pay the serialization cost.
- **Concurrency** — validators run in parallel; mutators run
sequentially in priority order.

### Package Structure

| Package | File | Responsibility |
|---------|------|----------------|
| `interceptors` | `interceptor.go` | Enums, Metadata, Interceptor
interface, Validator/Mutator structs, handler types, Invocation |
| `interceptors` | `wire.go` | Wire types: ListParams/Result,
InvokeParams/Result, InterceptorInfo, event/method constants |
| `interceptors` | `priority.go` | Priority with phase-aware resolution
and JSON marshaling |
| `interceptors` | `doc.go` | Package documentation with examples |
| `interceptors/chain` | `chain.go` | Chain engine: discovery,
filtering, sorting, execution orchestration |
| `interceptors/chain` | `result.go` | ExecutionResult, AbortInfo |
| `interceptors/chain` | `doc.go` | Sub-package documentation |
| `interceptors/mcpserver` | `server.go` | MCP server wrapper,
interceptor registry, capability advertisement, `LocalChain()` |
| `interceptors/mcpserver` | `rpc.go` | `interceptors/list` and
`interceptor/invoke` JSON-RPC handlers |
| `interceptors/mcpserver` | `doc.go` | Sub-package documentation |
| `integrations/gomiddleware` | `middleware.go` | go-sdk `Middleware`
that runs the chain on every request/response |
| `integrations/gomiddleware` | `errors.go` | Abort-to-JSONRPC error
conversion |
| `integrations/gomiddleware` | `doc.go` | Sub-package documentation |

### Documentation

- `doc/DESIGN.md` — architecture, execution model, integration with
go-sdk
- `doc/CONFORMANCE.md` — SEP conformance status
- `doc/PERFORMANCE.md` — per-request cost model

### Examples

- `examples/validator` — blocks dangerous tool calls, demonstrates
`WithContextProvider`
- `examples/mutator` — tags tool responses with audit trail

## Vendor: go-sdk v1.4.0 with custom method support

Vendors `go-sdk` with minimal extensions for custom JSON-RPC methods:

- `Server.AddCustomMethod` — registers a handler that bypasses the typed
pipeline
- `ClientSession.CallCustom` — sends raw JSON-RPC calls for custom
methods

These changes are generic (not interceptor-specific) and enable any MCP
extension to define its own JSON-RPC methods.

## TODO

- More integration tests
- Unit tests
- Support wildcards for event interception (separate PR)

---------

Signed-off-by: Kurt Degiorgio <kdegiorgio@bloomberg.net>
2026-04-29 15:29:25 +01:00

3 KiB

Go SDK Interceptors — Performance

Analysis of per-request costs and allocation patterns.


Design Rationale: JSON-RPC Invocation

The interceptor chain follows the SEP execution model: each interceptor is invoked via interceptor/invoke JSON-RPC calls through an MCP client session. For in-process interceptors, Server.LocalChain() creates an in-memory transport, so the JSON-RPC overhead is minimal (no network I/O).

Payloads are json.RawMessage at the protocol level. The middleware marshals request params and response results once per phase, and interceptor handlers unmarshal the specific fields they need.


Per-Request Cost Model

Every intercepted request passes through gomiddleware.Middleware. The cost depends on whether interceptors match the event.

Fast Path (skipped methods)

The middleware skips initialize, notifications/*, interceptors/list, and interceptor/invoke with a map lookup. Zero allocations, zero JSON operations.

Intercepted Path

With interceptors active, each active phase incurs:

Step Operation Allocations JSON ops
1 Marshal request params / response result 1 json.RawMessage 1 marshal
2 ChainExecutionResult struct (with pre-allocated Results slice) 1 struct + 1 slice 0
3 Per-interceptor interceptor/invoke RPC 1 InvokeParams + 1 InvokeResult per interceptor 1 marshal + 1 unmarshal per interceptor (handled by go-sdk JSON-RPC layer)
4 Validator execution (N=1 inline, N>1 goroutines) 0 (N=1) / goroutines (N>1) Handler-specific
5 Mutator payload threading 0 Payload passed as json.RawMessage between mutators
6 Unmarshal mutated payload back to params/result 0 1 unmarshal (if mutated)

In-Memory Transport Overhead

When using LocalChain(), the in-memory transport uses newline-delimited JSON over a net.Pipe(). Each interceptor/invoke call involves:

  1. JSON marshal of InvokeParams (client side)
  2. Write to pipe
  3. JSON unmarshal of InvokeParams (server side)
  4. Handler execution
  5. JSON marshal of InvokeResult (server side)
  6. Write to pipe
  7. JSON unmarshal of InvokeResult (client side)

The JSON-RPC overhead is bounded by the payload size and is negligible for typical MCP payloads. The in-memory pipe avoids network I/O entirely.

Audit Mode

Audit-mode mutators run normally but their mutated payload is not propagated to subsequent interceptors. No deep-copy is needed — the chain simply skips updating FinalPayload for audit-mode entries.


Optimization Opportunities

  1. Batch invoke: A future interceptor/invokeValidators method could reduce round-trips for validators that run in parallel.
  2. Payload caching: For validators that don't need the full payload, a lightweight invoke variant could skip payload serialization.
  3. Connection pooling: For remote interceptor servers, reusing client sessions across requests avoids connection setup overhead.