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>
This commit is contained in:
parent
8704137635
commit
57d4facc8f
1 changed files with 207 additions and 92 deletions
|
|
@ -1,4 +1,5 @@
|
|||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.IO.Pipes;
|
||||
using System.Security.Claims;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
|
@ -30,10 +31,46 @@ public class GatewayComponentsTests
|
|||
|
||||
bridge.Configure(serverOptions);
|
||||
|
||||
#pragma warning disable MCPEXP001
|
||||
Assert.NotNull(serverOptions.Capabilities.Extensions);
|
||||
Assert.True(serverOptions.Capabilities.Extensions!.ContainsKey(InterceptorProtocolConstants.ExtensionCapabilityKey));
|
||||
#pragma warning restore MCPEXP001
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("""{"name":"t"}""")] // missing inputSchema
|
||||
[InlineData("""{"name":"t","inputSchema":{}}""")] // empty object, no "type"
|
||||
[InlineData("""{"name":"t","inputSchema":{"type":"string"}}""")] // wrong type
|
||||
[InlineData("""{"name":"t","inputSchema":"not-an-object"}""")] // not a JSON object
|
||||
public void EnsureToolInputSchemas_CoercesMissingOrInvalidSchemaToSpecDefault(string toolJson)
|
||||
{
|
||||
var payload = JsonNode.Parse($$"""{"tools":[{{toolJson}}]}""");
|
||||
|
||||
GatewayProxyConfigurator.EnsureToolInputSchemas(payload);
|
||||
|
||||
var schema = payload!["tools"]![0]!["inputSchema"];
|
||||
Assert.Equal("object", schema!["type"]!.GetValue<string>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnsureToolInputSchemas_LeavesValidSchemaUntouched()
|
||||
{
|
||||
var payload = JsonNode.Parse(
|
||||
"""{"tools":[{"name":"t","inputSchema":{"type":"object","properties":{"x":{"type":"string"}},"required":["x"]}}]}""");
|
||||
var original = payload!["tools"]![0]!["inputSchema"]!.ToJsonString();
|
||||
|
||||
GatewayProxyConfigurator.EnsureToolInputSchemas(payload);
|
||||
|
||||
Assert.Equal(original, payload["tools"]![0]!["inputSchema"]!.ToJsonString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnsureToolInputSchemas_IgnoresPayloadWithoutToolsArray()
|
||||
{
|
||||
var payload = JsonNode.Parse("""{"resources":[]}""");
|
||||
|
||||
GatewayProxyConfigurator.EnsureToolInputSchemas(payload);
|
||||
|
||||
Assert.Null(payload!["tools"]);
|
||||
GatewayProxyConfigurator.EnsureToolInputSchemas(null);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -93,9 +130,7 @@ public class GatewayComponentsTests
|
|||
var serverOptions = new McpServerOptions();
|
||||
gateway.ConfigureServerOptions(serverOptions);
|
||||
|
||||
#pragma warning disable MCPEXP001
|
||||
Assert.True(serverOptions.Capabilities?.Extensions?.ContainsKey(InterceptorProtocolConstants.ExtensionCapabilityKey) ?? false);
|
||||
#pragma warning restore MCPEXP001
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -262,12 +297,10 @@ public class GatewayComponentsTests
|
|||
var filter = new InterceptorMessageFilter(collection);
|
||||
options.Filters.Message.IncomingFilters.Add(filter.CreateFilter);
|
||||
options.Capabilities ??= new();
|
||||
#pragma warning disable MCPEXP001
|
||||
options.Capabilities.Extensions ??= new Dictionary<string, object>();
|
||||
options.Capabilities.Extensions[InterceptorProtocolConstants.ExtensionCapabilityKey] = JsonSerializer.SerializeToElement(
|
||||
new InterceptorsCapability { SupportedEvents = [InterceptionEvents.ToolsCall] },
|
||||
InterceptorJsonUtilities.DefaultOptions);
|
||||
#pragma warning restore MCPEXP001
|
||||
});
|
||||
disposables.Add(interceptorServer);
|
||||
disposables.Add(interceptorClient);
|
||||
|
|
@ -301,12 +334,10 @@ public class GatewayComponentsTests
|
|||
var filter = new InterceptorMessageFilter(collection);
|
||||
serverOptions.Filters.Message.IncomingFilters.Add(filter.CreateFilter);
|
||||
serverOptions.Capabilities ??= new();
|
||||
#pragma warning disable MCPEXP001
|
||||
serverOptions.Capabilities.Extensions ??= new Dictionary<string, object>();
|
||||
serverOptions.Capabilities.Extensions[InterceptorProtocolConstants.ExtensionCapabilityKey] = JsonSerializer.SerializeToElement(
|
||||
new InterceptorsCapability { SupportedEvents = [InterceptionEvents.ToolsCall] },
|
||||
InterceptorJsonUtilities.DefaultOptions);
|
||||
#pragma warning restore MCPEXP001
|
||||
|
||||
var serverTransport = new StreamServerTransport(serverFromClient, serverToClient);
|
||||
var server = McpServer.Create(serverTransport, serverOptions);
|
||||
|
|
@ -360,7 +391,10 @@ public class GatewayComponentsTests
|
|||
public override Implementation? ClientInfo => null;
|
||||
public override McpServerOptions ServerOptions => new();
|
||||
public override IServiceProvider? Services => null;
|
||||
// Logging is deprecated upstream (SEP-2577) but remains part of the abstract surface.
|
||||
#pragma warning disable MCP9005, CS0672
|
||||
public override LoggingLevel? LoggingLevel => null;
|
||||
#pragma warning restore MCP9005, CS0672
|
||||
public override ValueTask DisposeAsync() => ValueTask.CompletedTask;
|
||||
public override IAsyncDisposable RegisterNotificationHandler(string method, Func<JsonRpcNotification, CancellationToken, ValueTask> handler) => throw new NotSupportedException();
|
||||
public override Task RunAsync(CancellationToken cancellationToken) => throw new NotSupportedException();
|
||||
|
|
@ -384,6 +418,7 @@ public class GatewayComponentsTests
|
|||
public override IAsyncDisposable RegisterNotificationHandler(string method, Func<JsonRpcNotification, CancellationToken, ValueTask> handler) => throw new NotSupportedException();
|
||||
public override Task SendMessageAsync(JsonRpcMessage message, CancellationToken cancellationToken = default) => throw new NotSupportedException();
|
||||
public override Task<JsonRpcResponse> SendRequestAsync(JsonRpcRequest request, CancellationToken cancellationToken = default) => throw new NotSupportedException();
|
||||
public override ValueTask<IDictionary<string, InputResponse>> ResolveInputRequestsAsync(IDictionary<string, InputRequest> inputRequests, CancellationToken cancellationToken) => throw new NotSupportedException();
|
||||
}
|
||||
|
||||
private sealed class TestInterceptor : McpServerInterceptor
|
||||
|
|
|
|||
Loading…
Reference in a new issue