Commit graph experimental-ext-interceptors/csharp
Author SHA1 Message Date
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
Peder Holdgaard Pedersen
28ada74189
C# SDK: multi-server interceptor chain orchestration per SEP (#18)
Addresses the third item of #15 ("Chain orchestrator only supports one
MCP server").

> **Stacked on #17** — this PR targets that branch because it builds on
the per-phase `PriorityHint` resolution introduced there. After #17
merges, the base will be switched to `main` (or it can be retargeted now
and rebased).

## What changed

The SEP's chain execution pattern requires: discover via
`interceptors/list` on one or more servers → merge & sort all
interceptors into a single chain by `priorityHint` (ascending,
alphabetical tie-break) → invoke each interceptor on the server that
hosts it (mutations sequential with payload chaining, validations
parallel) → aggregate one result. The SDK previously ran each client's
entire chain sequentially, so a `-1000` mutation on server 2 ran after a
`+1000` mutation on server 1, and each server's validations only saw its
own server's mutations.

- **Orchestrator** (`InterceptorChainOrchestrator`, internal): now
operates on chain entries — descriptor + invoker routing to its host —
mirroring the SEP's `ChainEntry`, with one global sort across all
filtered entries. The stable sort preserves caller-supplied server order
for exact ties.
- **New public API** (`InterceptorChain` / `InterceptorChainEntry`):
`DiscoverAsync` lists from every server in parallel (fail-closed: any
list failure throws rather than silently dropping a server's
interceptors), instance `ExecuteAsync` runs prebuilt entries (a future
caching seam), and a one-shot static `ExecuteAsync(servers, params)`
does discover + execute.
- **`InterceptorChainRunner`** (used by the gateway and
`InterceptingMcpClient`): one merged chain instead of the per-client
sequential loop. The single-client `client.ExecuteChainAsync(...)`
extension keeps its signature and delegates to the same path (still 1
list + N invokes).
- Duplicate interceptor names across servers are not deduplicated — each
entry invokes on its own host (results may share an `InterceptorName`;
documented).

## Behavior changes for multi-client gateways

- Interceptors from all servers form one globally-ordered chain
(previously server 1's full chain ran before server 2's).
- Validations from all servers run as one parallel batch against the
payload after **all** mutations.
- Sinks run once per phase for the merged chain (previously once per
client).
- `TimeoutMs` bounds the whole merged chain, not each per-client
sub-chain.
- Any server's `interceptors/list` failure fails the chain up-front
(fail-closed).
- Cost per phase: M parallel `interceptors/list` calls + N
`interceptor/invoke` calls.

## Testing

`dotnet test` from `csharp/sdk/`: 97 passing (was 87 after #17). New
coverage:
- Orchestrator: global priority ordering across servers, alphabetical
tie-break across servers, payload chaining across servers in one
mutation pass, all-server validations seeing the post-mutation payload,
duplicate names invoking once per host.
- `InterceptorChainTests` (real in-memory servers): discovery merge with
server attribution, merged execution order, fail-closed discovery,
single-client path equivalence.
- Gateway: two interceptor servers where the second hosts the
lower-priority mutation — the backend receives the globally-ordered
payload (fails under the old sequential runner).

Closes #15.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-08-06 17:22:33 +02:00
Peder Holdgaard Pedersen
7cf90c9ccb
C# SDK: align mode value and priorityHint with the SEP (#17)
Addresses the first two items of #15 (the chain-orchestrator
multi-server item follows in a separate PR).

## `mode`: keep `"active"`, add `"audit"` round-trip coverage

> **Update:** an earlier revision of this PR renamed `mode` from
`"active"` to `"enforce"`, following a SEP **draft**. The now-current
revised SEP-1763 ([PR
#2624](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2624))
defines `mode?: "active" | "audit"` with default `"active"` — so
`"active"` was correct all along. @jeongukjae flagged this; the rename
has been reverted (commit 58edf2c). The C# SDK's wire value `"active"`
matches the SEP and the TypeScript SDK.

`InterceptorMode` keeps members `Active` (wire `"active"`, default) and
`Audit` (wire `"audit"`). Added a deserialization test alongside the
existing serialization test so both wire forms are covered.

## `priorityHint`: support `number | { request?, response? }`

The SDK only supported a plain `int`. This adds a `PriorityHint`
protocol type whose converter accepts both wire forms and preserves them
on round-trip (a number stays a number, an object stays an object).
Chain ordering now resolves the effective priority per phase exactly as
the SEP's `resolvePriority` algorithm (unset → 0, alphabetical tie-break
unchanged).

- `Interceptor.PriorityHint` is now `PriorityHint?`; an implicit
conversion from `int` keeps existing code compiling.
- `[McpServerInterceptor]` gains `RequestPriorityHint` /
`ResponsePriorityHint` (mutually exclusive with the scalar
`PriorityHint`), following the official SDK's backing-field pattern for
optional attribute values.
- Behavior note: attribute-discovered interceptors with no priority set
now omit `priorityHint` from the wire instead of emitting `0` —
semantically identical per the SEP default, and consistent with the
existing default-skipping for `Mode`/`FailOpen`.

## Testing

`dotnet test` from `csharp/sdk/`: all passing, including new coverage
for both `priorityHint` wire forms, partial objects, unknown-key
tolerance, phase-dependent mutation ordering (the SEP's
pii-redactor/compressor example), the attribute conflict error, and both
`mode` wire values.

Part of #15.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 15:56:41 +02:00
Ukjae Jeong
99bc7c9bb0
Align capability key to SEP-2133 extensions format (#25)
Both SDKs now use Capabilities.Extensions with key
"io.modelcontextprotocol/interceptors". Previously C# used the short key
"interceptors" and Go used Capabilities.Experimental. SEP capability
example updated to match.

https://modelcontextprotocol.io/seps/2133-extensions

---------

Signed-off-by: Ukjae Jeong <jeongukjae@gmail.com>
2026-07-06 10:29:21 +01:00
Ukjae Jeong
2b1d10093e
Import revised sep and update the sep number (#21)
Interceptor SEP has been moved from
https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1763
to
https://github.com/modelcontextprotocol/modelcontextprotocol/issues/2624
.
This PR updates the SEP references over the codebase and import revised
SEP

Signed-off-by: Ukjae Jeong <jeongukjae@gmail.com>
2026-06-16 05:16:21 +01:00
Peder Holdgaard Pedersen
d01a626b76
C# Re-implementation (#5)
Re-implementation of C# interceptors SDK with clearer focus on
server-side / gateway interceptors.
Fixes a lot of issues with the existing implementation.
Still more boilerplate than I'm happy with, but this is better than what
was there. Will refine further in later PRs.
2026-05-14 15:11:41 +02:00
Peder Holdgaard Pedersen
45f9e0ef27
C# Implementation (#2)
C# implementation of SEP-1763

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 23:53:44 +00:00