experimental-ext-interceptors/csharp/sdk/docs/ARCHITECTURE_PHASES.md
Peder Holdgaard Pedersen 57d4facc8f
C# SDK: migrate to ModelContextProtocol 2.1.0 (MCP spec 2026-07-28) (#34)
## Summary

Bumps the MCP C# SDK dependency from 1.1.0 to 2.1.0 across all 9
projects and aligns the implementation with the 2.x surface and the
2026-07-28 spec — not just a version bump.

### Compile fixes
- Removed the gateway's `Capabilities.Tasks = null` suppression and its
test: the experimental Tasks API was removed from SDK core in 2.0 (moved
to a separate, wire-incompatible `ModelContextProtocol.Extensions.Tasks`
package).
- Added `ResolveInputRequestsAsync` (new abstract MRTR member on
`McpClient`) overrides to the test doubles. MRTR forwarding is
deliberately out of scope for this PR; the gateway's server reports
`IsMrtrSupported == false`.
- Deleted all `MCPEXP001` pragmas — `ServerCapabilities.Extensions`
(which carries `io.modelcontextprotocol/interceptors`) is stable in 2.x.
`MCPEXP002` subclassing pragmas are kept.

### Spec deprecations (SEP-2577 / SEP-2575)
- The gateway intentionally keeps proxying `logging/setLevel` and
`resources/subscribe` for down-level clients/backends, with targeted
`MCP9005` suppressions.
- On negotiated `2026-07-28` connections the SDK rejects these methods
at the session layer (replaced by per-request `_meta` log levels and
`subscriptions/listen`), so the affected tests now pin `protocolVersion:
"2025-06-18"` through a new fixture parameter — making them genuine
down-level-compatibility tests.

### Defensive hardening
- `Tool.inputSchema` is required and validated on deserialization in
2.x. The gateway's `ListToolsHandler` now fetches the backend
`tools/list` result as raw JSON and runs
`GatewayProxyConfigurator.EnsureToolInputSchemas` before and after the
interceptor chain, coercing missing/invalid schemas to the spec-default
`{"type":"object"}` so down-level backends and schema-stripping
interceptors can't fail the request.

### Tests & docs
- New tests: end-to-end schema-stripping coercion,
`EnsureToolInputSchemas` unit cases, and `2026-07-28` protocol-version
negotiation through the gateway (implicitly exercising
`server/discover`).
- `CLAUDE.md` gains an "SDK 2.x / spec 2026-07-28 notes" section;
`ARCHITECTURE_PHASES.md` no longer lists `Tasks` as a mirrored
capability.

## Test plan
- [x] `dotnet build` — 0 errors, 0 warnings (library, tests, all 7
samples)
- [x] `dotnet test` — 105/105 passing (was 98: −1 removed Tasks test, +8
new)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Ola Hungerford <olahungerford@gmail.com>
2026-08-18 17:20:51 -07:00

6.7 KiB

Architecture Phases

This document captures the phased structural changes planned for the C# MCP interceptors SDK so we can reference them during implementation.

Product Rule

The package should expose two distinct surfaces:

  1. WithInterceptors(...) / interceptor server: pure SEP surface
  2. McpInterceptorGateway: transparent proxy by default
  3. Gateway SEP passthrough: opt-in advanced mode
  4. Shared chain execution/runtime: reused by both surfaces

This lets the package support both:

  1. SEP-aware clients that explicitly use interceptors/list and interceptor/invoke (chain execution is SDK-side orchestration over those two)
  2. Transparent infrastructure deployments where clients do not need any code changes

Phase 1: Split Gateway Modes

Goal: make the gateway's public behavior explicit.

Changes:

  1. Add ExposeInterceptorProtocol to McpInterceptorGatewayOptions, defaulting to false
  2. In transparent mode, do not advertise interceptor extension capability
  3. In transparent mode, do not expose interceptors/list or interceptor/invoke
  4. In opt-in SEP mode, preserve the current passthrough behavior

Expected result:

  1. Default gateway looks like a backend proxy, not a backend-plus-admin endpoint
  2. Advanced users can still enable interceptor protocol passthrough intentionally

Acceptance criteria:

  1. Transparent mode does not advertise interceptor capability
  2. Transparent mode does not handle interceptor/* requests
  3. SEP mode preserves current passthrough behavior and tests

Phase 2: Make Capability Mirroring Truly Transparent

Goal: align the gateway's advertised contract with the backend server more faithfully.

Changes:

  1. Extract capability composition into a dedicated internal component or helper
  2. Start from the backend ServerCapabilities rather than hand-mirroring only selected properties
  3. Mirror standard and experimental capability surfaces, including Experimental and Extensions where present
  4. Overlay interceptor capability only when ExposeInterceptorProtocol = true

Expected result:

  1. The gateway remains aligned with the core SDK as new capabilities are added
  2. The gateway can honestly describe itself as transparent by default

Acceptance criteria:

  1. Transparent mode mirrors backend capabilities broadly
  2. Interceptor capability is only present in SEP mode
  3. Existing proxy behavior remains intact for supported primitives

Phase 3: Separate Proxy Core from SEP Bridge

Goal: separate transparent proxy concerns from SEP passthrough concerns.

Changes:

  1. Extract the built-in MCP proxying logic into a dedicated internal component
  2. Extract interceptor protocol passthrough into a dedicated internal bridge component
  3. Keep InterceptorChainRunner and related chain semantics shared
  4. Centralize interceptor extension capability key handling behind one internal constant/provider

Current implementation note:

  1. Transparent proxy wiring lives in GatewayProxyConfigurator
  2. SEP bridge wiring lives in GatewayInterceptorProtocolBridge
  3. The extension capability key is centralized in InterceptorProtocolConstants

Expected result:

  1. Transparent proxy and SEP bridge can evolve independently
  2. The gateway type becomes a coordinator rather than a monolith

Acceptance criteria:

  1. Proxy wiring can be enabled without SEP bridge wiring
  2. SEP bridge can be enabled or disabled explicitly
  3. Internal tests can target the components independently

Phase 4: Improve DI and Runtime Lifecycle Shape

Goal: align the builder path more closely with the core C# SDK's composition model.

Changes:

  1. Keep the current WithInterceptorGateway(options) overload for convenience
  2. Add a DI-native overload based on a factory or service-provider callback
  3. Refactor notification forwarding into an internal connection-bound forwarder abstraction
  4. Treat transport session identifiers as an implementation detail, not a public concept

Expected result:

  1. The builder path stays declarative and SDK-native
  2. Runtime ownership of backend/interceptor connections becomes easier to evolve
  3. The implementation remains compatible with current session-bearing transports without depending on sessions semantically

Acceptance criteria:

  1. Existing convenience builder API continues to work
  2. A service-provider-based configuration path exists
  3. Notification forwarding logic is isolated from proxy handler registration

Current implementation note:

  1. Builder DI now supports both direct options and Func<IServiceProvider, McpInterceptorGatewayOptions>
  2. Connection-bound forwarding registration lives in GatewayConnectionForwardingRegistrar
  3. Transport session identifiers are only used as an internal deduplication mechanism where available

Phase 5: Follow-Up Cleanup

Goal: tighten API roles after the bigger structural changes are done.

Changes:

  1. Keep extension methods as the primary low-level SEP client API
  2. Keep InterceptingMcpClient as a convenience/orchestration layer rather than the canonical abstraction
  3. Consider clarifying or making explicit wrapped-client disposal ownership
  4. Consolidate any duplicated transparent interception flow that remains after Phases 1-4

Expected result:

  1. Public API roles are clearer
  2. Future maintenance burden is lower

This phase is intentionally lower priority than Phases 1-4.

Future Feature: External SEP-Exposing Servers

This comes after the phase work above.

Goal: allow transparent gateways to source interceptors from external SEP servers rather than only from in-process/directly-provided clients.

Possible outcomes:

  1. Configuration-driven gateways (for example mcp-interceptors.json)
  2. Auth-aware or per-user interceptor selection
  3. Custom MCP middleware built on top of the extension's primitives

Likely direction:

  1. Introduce an interceptor client/provider abstraction in addition to direct InterceptorClients
  2. Support static, per-request, or per-identity interceptor resolution
  3. Keep chain semantics unchanged regardless of interceptor source

Current implementation note:

  1. McpInterceptorGateway.CreateAsync(...) now supports static external interceptor connections via McpInterceptorServerConnectionOptions
  2. Those connections use the same transport-driven pattern as McpClient.CreateAsync(...)
  3. Dynamic per-request interceptor resolution is now supported for the transparent proxy path via InterceptorServerConnectionResolver
  4. SEP passthrough still requires statically configured interceptor clients

Open Design Notes

  1. The interceptor extension capability key should stay aligned with the SEP for now
  2. If the SEP changes to a reverse-domain extension identifier, we should centralize the key and support a transition path
  3. Transparent mode should be the default because it matches the expected operational use of the gateway