experimental-ext-interceptors/go/sdk/doc/PERFORMANCE.md
Ukjae Jeong 8704137635
Go SDK: Upgrade MCP SDK and remove vendored copy (#35)
<!-- Provide a brief summary of your changes -->
Update the Go interceptor SDK to use
`github.com/modelcontextprotocol/go-sdk` v1.7.0 custom method APIs and
remove the vendored Go SDK copy. This shouldn't update the behaviours,
but addding following functions.

Changed public/user-facing interfaces:
extension.Extension.Install now returns an error:

```
// before
ext.Install(mcpServer)

// after
if err := ext.Install(mcpServer); err != nil {
	return err
}
```

New helper added:

```
extension.RegisterSendingMethods(client *mcp.Client) error
```

## Motivation and Context
<!-- Why is this change needed? What problem does it solve? -->

Pick up MCP Go SDK 1.7.0. This provides upstream custom method support,
so the local vendored SDK copy is no longer needed.

## How Has This Been Tested?
<!-- Have you tested this in a real application? Which scenarios were
tested? -->

- `go test -v ./...`

## Breaking Changes
<!-- Will users need to update their code or configurations? -->

```go
if err := ext.Install(mcpServer); err != nil {
    return err
}
```

The Go SDK module now requires Go 1.25 via
github.com/modelcontextprotocol/go-sdk v1.7.0.

## 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)
- [x] 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
- [x] 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
-->
LocalChain now registers interceptor custom sending methods internally.
Manual remote chain setup should call
`extension.RegisterSendingMethods(client)` before connecting the MCP
client session used with chain.AddMCPServer.

Signed-off-by: Ukjae Jeong <jeongukjae@gmail.com>
2026-08-12 14:48:17 -07:00

2.9 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, Extension.LocalChain(ctx, server) creates an in-memory transport, so there is 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 1 goroutine per validator 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.