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
17 changed files with 207 additions and 92 deletions
|
|
@ -1,21 +1,27 @@
|
||||||
# MCP Interceptors - C# SDK
|
# MCP Interceptors - C# SDK
|
||||||
|
|
||||||
## What this is
|
## What this is
|
||||||
C# implementation of gateway-level interceptors from [SEP-2624](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/2624). NuGet package additive to the official [C# MCP SDK](https://github.com/modelcontextprotocol/csharp-sdk) (v1.1.0). Focus is on the protocol-level extension (client → interceptor server → server), NOT in-process middleware.
|
C# implementation of gateway-level interceptors from [SEP-2624](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/2624). NuGet package additive to the official [C# MCP SDK](https://github.com/modelcontextprotocol/csharp-sdk) (v2.1.0, MCP spec 2026-07-28). Focus is on the protocol-level extension (client → interceptor server → server), NOT in-process middleware.
|
||||||
|
|
||||||
## Build & test
|
## Build & test
|
||||||
```
|
```
|
||||||
dotnet build # from csharp/sdk/
|
dotnet build # from csharp/sdk/
|
||||||
dotnet test # 98 tests across the interceptor test project
|
dotnet test # 105 tests across the interceptor test project
|
||||||
```
|
```
|
||||||
|
|
||||||
## Key architectural constraints
|
## Key architectural constraints
|
||||||
|
|
||||||
**Why message filter, not handlers**: `McpServerHandlers` and `McpServerImpl` are `internal` in the SDK. We can't register handlers for new JSON-RPC methods from outside. Instead we use `McpServerOptions.Filters.Message.IncomingFilters` to intercept `interceptors/list` and `interceptor/invoke`, handle them, send `JsonRpcResponse` via `context.Server.SendMessageAsync()`, and skip calling `next`. See `InterceptorMessageFilter.cs`. Chain execution is no longer a wire method — it's SDK orchestration in `Client/InterceptorChainOrchestrator.cs`.
|
**Why message filter, not handlers**: `McpServerHandlers` and `McpServerImpl` are `internal` in the SDK. We can't register handlers for new JSON-RPC methods from outside. Instead we use `McpServerOptions.Filters.Message.IncomingFilters` to intercept `interceptors/list` and `interceptor/invoke`, handle them, send `JsonRpcResponse` via `context.Server.SendMessageAsync()`, and skip calling `next`. See `InterceptorMessageFilter.cs`. Chain execution is no longer a wire method — it's SDK orchestration in `Client/InterceptorChainOrchestrator.cs`.
|
||||||
|
|
||||||
**Why `ServerCapabilities.Extensions`**: The SDK's intended mechanism for protocol extensions. Requires `#pragma warning disable MCPEXP001`. We advertise `InterceptorsCapability { SupportedEvents }` under `Extensions["io.modelcontextprotocol/interceptors"]`.
|
**Why `ServerCapabilities.Extensions`**: The SDK's intended mechanism for protocol extensions (stable since SDK 2.x — no experimental pragma needed). We advertise `InterceptorsCapability { SupportedEvents }` under `Extensions["io.modelcontextprotocol/interceptors"]`.
|
||||||
|
|
||||||
**Client `SendRequestAsync`**: The public overload (`McpSession.Methods.cs:24`) takes `JsonSerializerOptions`. We pass `InterceptorJsonUtilities.DefaultOptions` which chains `McpJsonUtilities.DefaultOptions` + our `InterceptorJsonContext`. The internal overload takes `JsonTypeInfo<T>` — we can't use it.
|
**Client `SendRequestAsync`**: The public `SendRequestAsync<TParameters, TResult>` endpoint extension takes `JsonSerializerOptions` (call it with named `cancellationToken:` — 2.x inserted an optional `RequestId` parameter before it). We pass `InterceptorJsonUtilities.DefaultOptions` which chains `McpJsonUtilities.DefaultOptions` + our `InterceptorJsonContext`. The internal overload takes `JsonTypeInfo<T>` — we can't use it.
|
||||||
|
|
||||||
|
**SDK 2.x / spec 2026-07-28 notes**:
|
||||||
|
- Tasks moved out of SDK core into a separate, wire-incompatible `ModelContextProtocol.Extensions.Tasks` package; the gateway no longer needs to suppress a backend `Tasks` capability.
|
||||||
|
- Logging (`logging/setLevel`) and `resources/subscribe` are deprecated by the spec (SEP-2577 / SEP-2575) and rejected on `2026-07-28` connections, but the gateway intentionally keeps proxying them for down-level clients/backends (`#pragma warning disable MCP9005` at those sites; tests pin `protocolVersion: "2025-06-18"`).
|
||||||
|
- `Tool.inputSchema` is required and validated on deserialization; the gateway fetches `tools/list` as raw JSON and coerces missing/invalid schemas to `{"type":"object"}` (`GatewayProxyConfigurator.EnsureToolInputSchemas`) so down-level backends and schema-stripping interceptors can't fail the request.
|
||||||
|
- MRTR (multi-round-trip requests) is not forwarded; the gateway's server reports `IsMrtrSupported == false`.
|
||||||
|
|
||||||
**`InterceptingMcpClient` is composition**: `McpClient` has an internal constructor; subclassing is `[Experimental]`. We wrap it as a concrete class exposing `.Inner` for direct access.
|
**`InterceptingMcpClient` is composition**: `McpClient` has an internal constructor; subclassing is `[Experimental]`. We wrap it as a concrete class exposing `.Inner` for direct access.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ Changes:
|
||||||
|
|
||||||
1. Extract capability composition into a dedicated internal component or helper
|
1. Extract capability composition into a dedicated internal component or helper
|
||||||
2. Start from the backend `ServerCapabilities` rather than hand-mirroring only selected properties
|
2. Start from the backend `ServerCapabilities` rather than hand-mirroring only selected properties
|
||||||
3. Mirror standard and experimental capability surfaces, including `Experimental`, `Extensions`, and `Tasks` where present
|
3. Mirror standard and experimental capability surfaces, including `Experimental` and `Extensions` where present
|
||||||
4. Overlay interceptor capability only when `ExposeInterceptorProtocol = true`
|
4. Overlay interceptor capability only when `ExposeInterceptorProtocol = true`
|
||||||
|
|
||||||
Expected result:
|
Expected result:
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="9.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="9.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.0" />
|
||||||
<PackageReference Include="ModelContextProtocol" Version="1.1.0" />
|
<PackageReference Include="ModelContextProtocol" Version="2.1.0" />
|
||||||
<ProjectReference Include="..\..\src\ModelContextProtocol.Interceptors\ModelContextProtocol.Interceptors.csproj" />
|
<ProjectReference Include="..\..\src\ModelContextProtocol.Interceptors\ModelContextProtocol.Interceptors.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="ModelContextProtocol" Version="1.1.0" />
|
<PackageReference Include="ModelContextProtocol" Version="2.1.0" />
|
||||||
<ProjectReference Include="..\..\src\ModelContextProtocol.Interceptors\ModelContextProtocol.Interceptors.csproj" />
|
<ProjectReference Include="..\..\src\ModelContextProtocol.Interceptors\ModelContextProtocol.Interceptors.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="ModelContextProtocol" Version="1.1.0" />
|
<PackageReference Include="ModelContextProtocol" Version="2.1.0" />
|
||||||
<ProjectReference Include="..\..\src\ModelContextProtocol.Interceptors\ModelContextProtocol.Interceptors.csproj" />
|
<ProjectReference Include="..\..\src\ModelContextProtocol.Interceptors\ModelContextProtocol.Interceptors.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="ModelContextProtocol" Version="1.1.0" />
|
<PackageReference Include="ModelContextProtocol" Version="2.1.0" />
|
||||||
<ProjectReference Include="..\..\src\ModelContextProtocol.Interceptors\ModelContextProtocol.Interceptors.csproj" />
|
<ProjectReference Include="..\..\src\ModelContextProtocol.Interceptors\ModelContextProtocol.Interceptors.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="ModelContextProtocol" Version="1.1.0" />
|
<PackageReference Include="ModelContextProtocol" Version="2.1.0" />
|
||||||
<ProjectReference Include="..\..\src\ModelContextProtocol.Interceptors\ModelContextProtocol.Interceptors.csproj" />
|
<ProjectReference Include="..\..\src\ModelContextProtocol.Interceptors\ModelContextProtocol.Interceptors.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.0" />
|
||||||
<PackageReference Include="ModelContextProtocol" Version="1.1.0" />
|
<PackageReference Include="ModelContextProtocol" Version="2.1.0" />
|
||||||
<ProjectReference Include="..\..\src\ModelContextProtocol.Interceptors\ModelContextProtocol.Interceptors.csproj" />
|
<ProjectReference Include="..\..\src\ModelContextProtocol.Interceptors\ModelContextProtocol.Interceptors.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="ModelContextProtocol" Version="1.1.0" />
|
<PackageReference Include="ModelContextProtocol" Version="2.1.0" />
|
||||||
<ProjectReference Include="..\..\src\ModelContextProtocol.Interceptors\ModelContextProtocol.Interceptors.csproj" />
|
<ProjectReference Include="..\..\src\ModelContextProtocol.Interceptors\ModelContextProtocol.Interceptors.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,6 @@ internal sealed class GatewayInterceptorProtocolBridge
|
||||||
|
|
||||||
foreach (var client in _interceptorClients)
|
foreach (var client in _interceptorClients)
|
||||||
{
|
{
|
||||||
#pragma warning disable MCPEXP001
|
|
||||||
if (client.ServerCapabilities?.Extensions is { } extensions &&
|
if (client.ServerCapabilities?.Extensions is { } extensions &&
|
||||||
extensions.TryGetValue(InterceptorProtocolConstants.ExtensionCapabilityKey, out var capObj))
|
extensions.TryGetValue(InterceptorProtocolConstants.ExtensionCapabilityKey, out var capObj))
|
||||||
{
|
{
|
||||||
|
|
@ -59,7 +58,6 @@ internal sealed class GatewayInterceptorProtocolBridge
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#pragma warning restore MCPEXP001
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!anyInterceptorCapabilityFound)
|
if (!anyInterceptorCapabilityFound)
|
||||||
|
|
@ -67,13 +65,11 @@ internal sealed class GatewayInterceptorProtocolBridge
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
#pragma warning disable MCPEXP001
|
|
||||||
serverOptions.Capabilities ??= new ServerCapabilities();
|
serverOptions.Capabilities ??= new ServerCapabilities();
|
||||||
serverOptions.Capabilities.Extensions ??= new Dictionary<string, object>();
|
serverOptions.Capabilities.Extensions ??= new Dictionary<string, object>();
|
||||||
serverOptions.Capabilities.Extensions[InterceptorProtocolConstants.ExtensionCapabilityKey] = JsonSerializer.SerializeToElement(
|
serverOptions.Capabilities.Extensions[InterceptorProtocolConstants.ExtensionCapabilityKey] = JsonSerializer.SerializeToElement(
|
||||||
new InterceptorsCapability { SupportedEvents = allEvents.ToList() },
|
new InterceptorsCapability { SupportedEvents = allEvents.ToList() },
|
||||||
InterceptorJsonUtilities.DefaultOptions);
|
InterceptorJsonUtilities.DefaultOptions);
|
||||||
#pragma warning restore MCPEXP001
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ConfigurePassthroughFilter(McpServerOptions serverOptions)
|
private void ConfigurePassthroughFilter(McpServerOptions serverOptions)
|
||||||
|
|
|
||||||
|
|
@ -49,11 +49,6 @@ internal sealed class GatewayProxyConfigurator
|
||||||
? CloneCapabilities(backendCaps)
|
? CloneCapabilities(backendCaps)
|
||||||
: serverOptions.Capabilities ?? new ServerCapabilities();
|
: serverOptions.Capabilities ?? new ServerCapabilities();
|
||||||
|
|
||||||
// Task passthrough is not implemented yet, so do not advertise backend task support.
|
|
||||||
#pragma warning disable MCPEXP001
|
|
||||||
serverOptions.Capabilities.Tasks = null;
|
|
||||||
#pragma warning restore MCPEXP001
|
|
||||||
|
|
||||||
ConfigureTools(serverOptions, backendCaps);
|
ConfigureTools(serverOptions, backendCaps);
|
||||||
ConfigurePrompts(serverOptions, backendCaps);
|
ConfigurePrompts(serverOptions, backendCaps);
|
||||||
ConfigureResources(serverOptions, backendCaps);
|
ConfigureResources(serverOptions, backendCaps);
|
||||||
|
|
@ -85,15 +80,23 @@ internal sealed class GatewayProxyConfigurator
|
||||||
|
|
||||||
var mutatedParams = JsonSerializer.Deserialize<ListToolsRequestParams>(requestPayload, _jsonOptions)
|
var mutatedParams = JsonSerializer.Deserialize<ListToolsRequestParams>(requestPayload, _jsonOptions)
|
||||||
?? request.Params!;
|
?? request.Params!;
|
||||||
var result = await _backend.ListToolsAsync(mutatedParams, ct);
|
|
||||||
|
// Fetch the backend result as raw JSON: deserializing into ListToolsResult rejects
|
||||||
|
// tools whose inputSchema is missing or invalid, which would fail the whole request
|
||||||
|
// for a down-level backend before the proxy could compensate.
|
||||||
|
var backendNode = await _backend.SendRequestAsync<ListToolsRequestParams, JsonNode>(
|
||||||
|
RequestMethods.ToolsList, mutatedParams, _jsonOptions, cancellationToken: ct);
|
||||||
|
EnsureToolInputSchemas(backendNode);
|
||||||
|
var result = JsonSerializer.Deserialize<ListToolsResult>(backendNode, _jsonOptions)
|
||||||
|
?? throw new InvalidOperationException("The backend returned a null tools/list result.");
|
||||||
|
|
||||||
if (chainRunner.ShouldIntercept(InterceptionEvents.ToolsList))
|
if (chainRunner.ShouldIntercept(InterceptionEvents.ToolsList))
|
||||||
{
|
{
|
||||||
var responsePayload = JsonSerializer.SerializeToNode(result, _jsonOptions)!;
|
|
||||||
var (processed, responseStatus) = await chainRunner.RunChainPhaseAsync(
|
var (processed, responseStatus) = await chainRunner.RunChainPhaseAsync(
|
||||||
InterceptionEvents.ToolsList, InterceptorPhase.Response, responsePayload, ct);
|
InterceptionEvents.ToolsList, InterceptorPhase.Response, backendNode, ct);
|
||||||
if (responseStatus != InterceptorChainStatus.Success)
|
if (responseStatus != InterceptorChainStatus.Success)
|
||||||
InterceptorChainRunner.ThrowChainFailure("tools/list", InterceptorPhase.Response, responseStatus);
|
InterceptorChainRunner.ThrowChainFailure("tools/list", InterceptorPhase.Response, responseStatus);
|
||||||
|
EnsureToolInputSchemas(processed);
|
||||||
result = JsonSerializer.Deserialize<ListToolsResult>(processed, _jsonOptions) ?? result;
|
result = JsonSerializer.Deserialize<ListToolsResult>(processed, _jsonOptions) ?? result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -321,6 +324,9 @@ internal sealed class GatewayProxyConfigurator
|
||||||
await _backend.CompleteAsync(request.Params!, ct);
|
await _backend.CompleteAsync(request.Params!, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Logging is deprecated upstream (SEP-2577) but the gateway intentionally keeps proxying it
|
||||||
|
// for down-level clients/backends.
|
||||||
|
#pragma warning disable MCP9005
|
||||||
private void ConfigureLogging(McpServerOptions serverOptions, ServerCapabilities? backendCaps)
|
private void ConfigureLogging(McpServerOptions serverOptions, ServerCapabilities? backendCaps)
|
||||||
{
|
{
|
||||||
if (backendCaps?.Logging is null)
|
if (backendCaps?.Logging is null)
|
||||||
|
|
@ -334,6 +340,38 @@ internal sealed class GatewayProxyConfigurator
|
||||||
return new EmptyResult();
|
return new EmptyResult();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
#pragma warning restore MCP9005
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// SDK 2.x requires tools/list entries to carry an inputSchema that is a JSON object with
|
||||||
|
/// "type":"object"; missing schemas throw during deserialization and invalid ones are
|
||||||
|
/// rejected by the Tool setter. As a transparent proxy we coerce missing or invalid schemas
|
||||||
|
/// from down-level backends or interceptor mutations to the spec-default {"type":"object"}.
|
||||||
|
/// </summary>
|
||||||
|
internal static void EnsureToolInputSchemas(JsonNode? listToolsPayload)
|
||||||
|
{
|
||||||
|
if (listToolsPayload?["tools"] is not JsonArray tools)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var node in tools)
|
||||||
|
{
|
||||||
|
if (node is not JsonObject tool)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var valid = tool["inputSchema"] is JsonObject schema
|
||||||
|
&& schema["type"] is JsonValue typeValue
|
||||||
|
&& typeValue.TryGetValue<string>(out var type)
|
||||||
|
&& type == "object";
|
||||||
|
if (!valid)
|
||||||
|
{
|
||||||
|
tool["inputSchema"] = new JsonObject { ["type"] = "object" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private ServerCapabilities CloneCapabilities(ServerCapabilities capabilities)
|
private ServerCapabilities CloneCapabilities(ServerCapabilities capabilities)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -185,7 +185,6 @@ internal sealed class InterceptorServerOptionsSetup : IConfigureOptions<McpServe
|
||||||
// Advertise interceptor capability via Extensions
|
// Advertise interceptor capability via Extensions
|
||||||
options.Capabilities ??= new();
|
options.Capabilities ??= new();
|
||||||
|
|
||||||
#pragma warning disable MCPEXP001 // We intentionally use the experimental Extensions API for protocol extensions
|
|
||||||
options.Capabilities.Extensions ??= new Dictionary<string, object>();
|
options.Capabilities.Extensions ??= new Dictionary<string, object>();
|
||||||
// Serialize to JsonElement so the SDK's own serializer (which doesn't know about
|
// Serialize to JsonElement so the SDK's own serializer (which doesn't know about
|
||||||
// InterceptorsCapability) can write it without type-info issues.
|
// InterceptorsCapability) can write it without type-info issues.
|
||||||
|
|
@ -195,6 +194,5 @@ internal sealed class InterceptorServerOptionsSetup : IConfigureOptions<McpServe
|
||||||
};
|
};
|
||||||
options.Capabilities.Extensions[InterceptorProtocolConstants.ExtensionCapabilityKey] = JsonSerializer.SerializeToElement(
|
options.Capabilities.Extensions[InterceptorProtocolConstants.ExtensionCapabilityKey] = JsonSerializer.SerializeToElement(
|
||||||
capability, InterceptorJsonUtilities.DefaultOptions);
|
capability, InterceptorJsonUtilities.DefaultOptions);
|
||||||
#pragma warning restore MCPEXP001
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="ModelContextProtocol" Version="1.1.0" />
|
<PackageReference Include="ModelContextProtocol" Version="2.1.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Nodes;
|
||||||
using System.IO.Pipes;
|
using System.IO.Pipes;
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
@ -30,10 +31,46 @@ public class GatewayComponentsTests
|
||||||
|
|
||||||
bridge.Configure(serverOptions);
|
bridge.Configure(serverOptions);
|
||||||
|
|
||||||
#pragma warning disable MCPEXP001
|
|
||||||
Assert.NotNull(serverOptions.Capabilities.Extensions);
|
Assert.NotNull(serverOptions.Capabilities.Extensions);
|
||||||
Assert.True(serverOptions.Capabilities.Extensions!.ContainsKey(InterceptorProtocolConstants.ExtensionCapabilityKey));
|
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]
|
[Fact]
|
||||||
|
|
@ -93,9 +130,7 @@ public class GatewayComponentsTests
|
||||||
var serverOptions = new McpServerOptions();
|
var serverOptions = new McpServerOptions();
|
||||||
gateway.ConfigureServerOptions(serverOptions);
|
gateway.ConfigureServerOptions(serverOptions);
|
||||||
|
|
||||||
#pragma warning disable MCPEXP001
|
|
||||||
Assert.True(serverOptions.Capabilities?.Extensions?.ContainsKey(InterceptorProtocolConstants.ExtensionCapabilityKey) ?? false);
|
Assert.True(serverOptions.Capabilities?.Extensions?.ContainsKey(InterceptorProtocolConstants.ExtensionCapabilityKey) ?? false);
|
||||||
#pragma warning restore MCPEXP001
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -262,12 +297,10 @@ public class GatewayComponentsTests
|
||||||
var filter = new InterceptorMessageFilter(collection);
|
var filter = new InterceptorMessageFilter(collection);
|
||||||
options.Filters.Message.IncomingFilters.Add(filter.CreateFilter);
|
options.Filters.Message.IncomingFilters.Add(filter.CreateFilter);
|
||||||
options.Capabilities ??= new();
|
options.Capabilities ??= new();
|
||||||
#pragma warning disable MCPEXP001
|
|
||||||
options.Capabilities.Extensions ??= new Dictionary<string, object>();
|
options.Capabilities.Extensions ??= new Dictionary<string, object>();
|
||||||
options.Capabilities.Extensions[InterceptorProtocolConstants.ExtensionCapabilityKey] = JsonSerializer.SerializeToElement(
|
options.Capabilities.Extensions[InterceptorProtocolConstants.ExtensionCapabilityKey] = JsonSerializer.SerializeToElement(
|
||||||
new InterceptorsCapability { SupportedEvents = [InterceptionEvents.ToolsCall] },
|
new InterceptorsCapability { SupportedEvents = [InterceptionEvents.ToolsCall] },
|
||||||
InterceptorJsonUtilities.DefaultOptions);
|
InterceptorJsonUtilities.DefaultOptions);
|
||||||
#pragma warning restore MCPEXP001
|
|
||||||
});
|
});
|
||||||
disposables.Add(interceptorServer);
|
disposables.Add(interceptorServer);
|
||||||
disposables.Add(interceptorClient);
|
disposables.Add(interceptorClient);
|
||||||
|
|
@ -301,12 +334,10 @@ public class GatewayComponentsTests
|
||||||
var filter = new InterceptorMessageFilter(collection);
|
var filter = new InterceptorMessageFilter(collection);
|
||||||
serverOptions.Filters.Message.IncomingFilters.Add(filter.CreateFilter);
|
serverOptions.Filters.Message.IncomingFilters.Add(filter.CreateFilter);
|
||||||
serverOptions.Capabilities ??= new();
|
serverOptions.Capabilities ??= new();
|
||||||
#pragma warning disable MCPEXP001
|
|
||||||
serverOptions.Capabilities.Extensions ??= new Dictionary<string, object>();
|
serverOptions.Capabilities.Extensions ??= new Dictionary<string, object>();
|
||||||
serverOptions.Capabilities.Extensions[InterceptorProtocolConstants.ExtensionCapabilityKey] = JsonSerializer.SerializeToElement(
|
serverOptions.Capabilities.Extensions[InterceptorProtocolConstants.ExtensionCapabilityKey] = JsonSerializer.SerializeToElement(
|
||||||
new InterceptorsCapability { SupportedEvents = [InterceptionEvents.ToolsCall] },
|
new InterceptorsCapability { SupportedEvents = [InterceptionEvents.ToolsCall] },
|
||||||
InterceptorJsonUtilities.DefaultOptions);
|
InterceptorJsonUtilities.DefaultOptions);
|
||||||
#pragma warning restore MCPEXP001
|
|
||||||
|
|
||||||
var serverTransport = new StreamServerTransport(serverFromClient, serverToClient);
|
var serverTransport = new StreamServerTransport(serverFromClient, serverToClient);
|
||||||
var server = McpServer.Create(serverTransport, serverOptions);
|
var server = McpServer.Create(serverTransport, serverOptions);
|
||||||
|
|
@ -360,7 +391,10 @@ public class GatewayComponentsTests
|
||||||
public override Implementation? ClientInfo => null;
|
public override Implementation? ClientInfo => null;
|
||||||
public override McpServerOptions ServerOptions => new();
|
public override McpServerOptions ServerOptions => new();
|
||||||
public override IServiceProvider? Services => null;
|
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;
|
public override LoggingLevel? LoggingLevel => null;
|
||||||
|
#pragma warning restore MCP9005, CS0672
|
||||||
public override ValueTask DisposeAsync() => ValueTask.CompletedTask;
|
public override ValueTask DisposeAsync() => ValueTask.CompletedTask;
|
||||||
public override IAsyncDisposable RegisterNotificationHandler(string method, Func<JsonRpcNotification, CancellationToken, ValueTask> handler) => throw new NotSupportedException();
|
public override IAsyncDisposable RegisterNotificationHandler(string method, Func<JsonRpcNotification, CancellationToken, ValueTask> handler) => throw new NotSupportedException();
|
||||||
public override Task RunAsync(CancellationToken cancellationToken) => 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 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 SendMessageAsync(JsonRpcMessage message, CancellationToken cancellationToken = default) => throw new NotSupportedException();
|
||||||
public override Task<JsonRpcResponse> SendRequestAsync(JsonRpcRequest request, 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
|
private sealed class TestInterceptor : McpServerInterceptor
|
||||||
|
|
|
||||||
|
|
@ -195,12 +195,10 @@ public class InterceptorChainTests
|
||||||
options.Filters.Message.IncomingFilters.Add(filter.CreateFilter);
|
options.Filters.Message.IncomingFilters.Add(filter.CreateFilter);
|
||||||
|
|
||||||
options.Capabilities ??= new();
|
options.Capabilities ??= new();
|
||||||
#pragma warning disable MCPEXP001
|
|
||||||
options.Capabilities.Extensions ??= new Dictionary<string, object>();
|
options.Capabilities.Extensions ??= new Dictionary<string, object>();
|
||||||
options.Capabilities.Extensions[InterceptorProtocolConstants.ExtensionCapabilityKey] = JsonSerializer.SerializeToElement(
|
options.Capabilities.Extensions[InterceptorProtocolConstants.ExtensionCapabilityKey] = JsonSerializer.SerializeToElement(
|
||||||
new InterceptorsCapability { SupportedEvents = [InterceptionEvents.All] },
|
new InterceptorsCapability { SupportedEvents = [InterceptionEvents.All] },
|
||||||
InterceptorJsonUtilities.DefaultOptions);
|
InterceptorJsonUtilities.DefaultOptions);
|
||||||
#pragma warning restore MCPEXP001
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async ValueTask DisposeAsync()
|
public async ValueTask DisposeAsync()
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,75 @@ public class McpInterceptorGatewayTests
|
||||||
Assert.Equal("test-tool", tools[0].Name);
|
Assert.Equal("test-tool", tools[0].Name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Gateway_NegotiatesLatestProtocolVersion()
|
||||||
|
{
|
||||||
|
await using var fixture = await GatewayTestFixture.CreateAsync(
|
||||||
|
backendConfigure: (options) =>
|
||||||
|
{
|
||||||
|
options.Capabilities ??= new();
|
||||||
|
options.Capabilities.Tools ??= new();
|
||||||
|
options.Handlers.ListToolsHandler = (request, ct) =>
|
||||||
|
new ValueTask<ListToolsResult>(new ListToolsResult { Tools = [] });
|
||||||
|
options.Handlers.CallToolHandler = (request, ct) =>
|
||||||
|
new ValueTask<CallToolResult>(new CallToolResult());
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.Equal("2026-07-28", fixture.ProxyClient.NegotiatedProtocolVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ListTools_InjectsDefaultInputSchemaWhenInterceptorStripsIt()
|
||||||
|
{
|
||||||
|
await using var fixture = await GatewayTestFixture.CreateAsync(
|
||||||
|
backendConfigure: (options) =>
|
||||||
|
{
|
||||||
|
options.Capabilities ??= new();
|
||||||
|
options.Capabilities.Tools ??= new();
|
||||||
|
options.Handlers.ListToolsHandler = (request, ct) =>
|
||||||
|
{
|
||||||
|
return new ValueTask<ListToolsResult>(new ListToolsResult
|
||||||
|
{
|
||||||
|
Tools = [new Tool { Name = "stripped-tool", Description = "Loses its schema" }],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
options.Handlers.CallToolHandler = (request, ct) =>
|
||||||
|
new ValueTask<CallToolResult>(new CallToolResult());
|
||||||
|
},
|
||||||
|
interceptors: [CreateMutationInterceptor("schema-stripper", (req, _, _, _) =>
|
||||||
|
{
|
||||||
|
// Only the response phase carries a tools array; pass the request phase through.
|
||||||
|
if (req.Payload?["tools"] is not JsonArray)
|
||||||
|
{
|
||||||
|
return new ValueTask<InterceptorResult>(new MutationInterceptorResult
|
||||||
|
{
|
||||||
|
Modified = false,
|
||||||
|
Payload = req.Payload,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload = req.Payload!.DeepClone();
|
||||||
|
foreach (var tool in payload["tools"]!.AsArray())
|
||||||
|
{
|
||||||
|
tool!.AsObject().Remove("inputSchema");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ValueTask<InterceptorResult>(new MutationInterceptorResult
|
||||||
|
{
|
||||||
|
Modified = true,
|
||||||
|
Payload = payload,
|
||||||
|
});
|
||||||
|
}, events: [InterceptionEvents.ToolsList])]);
|
||||||
|
|
||||||
|
// Without the gateway coercing the stripped inputSchema back to the spec default,
|
||||||
|
// deserializing the mutated payload would fail and the request would error out.
|
||||||
|
var tools = await fixture.ProxyClient.ListToolsAsync();
|
||||||
|
Assert.Single(tools);
|
||||||
|
Assert.Equal("stripped-tool", tools[0].Name);
|
||||||
|
Assert.Equal(JsonValueKind.Object, tools[0].JsonSchema.ValueKind);
|
||||||
|
Assert.Equal("object", tools[0].JsonSchema.GetProperty("type").GetString());
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task CallToolAsync_PassesThroughInterceptors()
|
public async Task CallToolAsync_PassesThroughInterceptors()
|
||||||
{
|
{
|
||||||
|
|
@ -223,33 +292,6 @@ public class McpInterceptorGatewayTests
|
||||||
Assert.True(caps.Experimental!.ContainsKey("com.example/feature"));
|
Assert.True(caps.Experimental!.ContainsKey("com.example/feature"));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task ConfigureServerOptions_DoesNotAdvertiseBackendTasksCapability()
|
|
||||||
{
|
|
||||||
await using var fixture = await GatewayTestFixture.CreateAsync(
|
|
||||||
backendConfigure: (options) =>
|
|
||||||
{
|
|
||||||
options.Capabilities ??= new();
|
|
||||||
options.Capabilities.Tools ??= new();
|
|
||||||
#pragma warning disable MCPEXP001
|
|
||||||
options.Capabilities.Tasks = new McpTasksCapability
|
|
||||||
{
|
|
||||||
List = new ListMcpTasksCapability(),
|
|
||||||
Cancel = new CancelMcpTasksCapability(),
|
|
||||||
};
|
|
||||||
#pragma warning restore MCPEXP001
|
|
||||||
options.Handlers.ListToolsHandler = (request, ct) =>
|
|
||||||
new ValueTask<ListToolsResult>(new ListToolsResult { Tools = [] });
|
|
||||||
options.Handlers.CallToolHandler = (request, ct) =>
|
|
||||||
new ValueTask<CallToolResult>(new CallToolResult());
|
|
||||||
});
|
|
||||||
|
|
||||||
var caps = fixture.ProxyClient.ServerCapabilities;
|
|
||||||
#pragma warning disable MCPEXP001
|
|
||||||
Assert.Null(caps.Tasks);
|
|
||||||
#pragma warning restore MCPEXP001
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ConfigureServerOptions_MirrorsBackendExtensionsInTransparentMode()
|
public async Task ConfigureServerOptions_MirrorsBackendExtensionsInTransparentMode()
|
||||||
{
|
{
|
||||||
|
|
@ -258,22 +300,18 @@ public class McpInterceptorGatewayTests
|
||||||
{
|
{
|
||||||
options.Capabilities ??= new();
|
options.Capabilities ??= new();
|
||||||
options.Capabilities.Tools ??= new();
|
options.Capabilities.Tools ??= new();
|
||||||
#pragma warning disable MCPEXP001
|
|
||||||
options.Capabilities.Extensions ??= new Dictionary<string, object>();
|
options.Capabilities.Extensions ??= new Dictionary<string, object>();
|
||||||
options.Capabilities.Extensions["com.example/backend"] = JsonSerializer.SerializeToElement(new { version = 1 });
|
options.Capabilities.Extensions["com.example/backend"] = JsonSerializer.SerializeToElement(new { version = 1 });
|
||||||
#pragma warning restore MCPEXP001
|
|
||||||
options.Handlers.ListToolsHandler = (request, ct) =>
|
options.Handlers.ListToolsHandler = (request, ct) =>
|
||||||
new ValueTask<ListToolsResult>(new ListToolsResult { Tools = [] });
|
new ValueTask<ListToolsResult>(new ListToolsResult { Tools = [] });
|
||||||
options.Handlers.CallToolHandler = (request, ct) =>
|
options.Handlers.CallToolHandler = (request, ct) =>
|
||||||
new ValueTask<CallToolResult>(new CallToolResult());
|
new ValueTask<CallToolResult>(new CallToolResult());
|
||||||
});
|
});
|
||||||
|
|
||||||
#pragma warning disable MCPEXP001
|
|
||||||
var caps = fixture.ProxyClient.ServerCapabilities;
|
var caps = fixture.ProxyClient.ServerCapabilities;
|
||||||
Assert.NotNull(caps.Extensions);
|
Assert.NotNull(caps.Extensions);
|
||||||
Assert.True(caps.Extensions!.ContainsKey("com.example/backend"));
|
Assert.True(caps.Extensions!.ContainsKey("com.example/backend"));
|
||||||
Assert.False(caps.Extensions.ContainsKey("io.modelcontextprotocol/interceptors"));
|
Assert.False(caps.Extensions.ContainsKey("io.modelcontextprotocol/interceptors"));
|
||||||
#pragma warning restore MCPEXP001
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -290,10 +328,8 @@ public class McpInterceptorGatewayTests
|
||||||
new ValueTask<CallToolResult>(new CallToolResult());
|
new ValueTask<CallToolResult>(new CallToolResult());
|
||||||
});
|
});
|
||||||
|
|
||||||
#pragma warning disable MCPEXP001
|
|
||||||
var caps = fixture.ProxyClient.ServerCapabilities;
|
var caps = fixture.ProxyClient.ServerCapabilities;
|
||||||
Assert.False(caps?.Extensions?.ContainsKey("io.modelcontextprotocol/interceptors") ?? false);
|
Assert.False(caps?.Extensions?.ContainsKey("io.modelcontextprotocol/interceptors") ?? false);
|
||||||
#pragma warning restore MCPEXP001
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -304,10 +340,8 @@ public class McpInterceptorGatewayTests
|
||||||
{
|
{
|
||||||
options.Capabilities ??= new();
|
options.Capabilities ??= new();
|
||||||
options.Capabilities.Tools ??= new();
|
options.Capabilities.Tools ??= new();
|
||||||
#pragma warning disable MCPEXP001
|
|
||||||
options.Capabilities.Extensions ??= new Dictionary<string, object>();
|
options.Capabilities.Extensions ??= new Dictionary<string, object>();
|
||||||
options.Capabilities.Extensions["com.example/backend"] = JsonSerializer.SerializeToElement(new { version = 1 });
|
options.Capabilities.Extensions["com.example/backend"] = JsonSerializer.SerializeToElement(new { version = 1 });
|
||||||
#pragma warning restore MCPEXP001
|
|
||||||
options.Handlers.ListToolsHandler = (request, ct) =>
|
options.Handlers.ListToolsHandler = (request, ct) =>
|
||||||
new ValueTask<ListToolsResult>(new ListToolsResult { Tools = [] });
|
new ValueTask<ListToolsResult>(new ListToolsResult { Tools = [] });
|
||||||
options.Handlers.CallToolHandler = (request, ct) =>
|
options.Handlers.CallToolHandler = (request, ct) =>
|
||||||
|
|
@ -315,12 +349,10 @@ public class McpInterceptorGatewayTests
|
||||||
},
|
},
|
||||||
exposeInterceptorProtocol: true);
|
exposeInterceptorProtocol: true);
|
||||||
|
|
||||||
#pragma warning disable MCPEXP001
|
|
||||||
var caps = fixture.ProxyClient.ServerCapabilities;
|
var caps = fixture.ProxyClient.ServerCapabilities;
|
||||||
Assert.NotNull(caps?.Extensions);
|
Assert.NotNull(caps?.Extensions);
|
||||||
Assert.True(caps!.Extensions!.ContainsKey("io.modelcontextprotocol/interceptors"));
|
Assert.True(caps!.Extensions!.ContainsKey("io.modelcontextprotocol/interceptors"));
|
||||||
Assert.True(caps.Extensions.ContainsKey("com.example/backend"));
|
Assert.True(caps.Extensions.ContainsKey("com.example/backend"));
|
||||||
#pragma warning restore MCPEXP001
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -538,7 +570,10 @@ public class McpInterceptorGatewayTests
|
||||||
Modified = true,
|
Modified = true,
|
||||||
Payload = JsonNode.Parse(payloadStr),
|
Payload = JsonNode.Parse(payloadStr),
|
||||||
});
|
});
|
||||||
}, events: [InterceptionEvents.ResourcesSubscribe])]);
|
}, events: [InterceptionEvents.ResourcesSubscribe])],
|
||||||
|
// resources/subscribe was replaced by subscriptions/listen in spec 2026-07-28;
|
||||||
|
// the gateway keeps proxying it for down-level connections, so pin one here.
|
||||||
|
protocolVersion: "2025-06-18");
|
||||||
|
|
||||||
await fixture.ProxyClient.SubscribeToResourceAsync("resource://original");
|
await fixture.ProxyClient.SubscribeToResourceAsync("resource://original");
|
||||||
|
|
||||||
|
|
@ -773,9 +808,7 @@ public class McpInterceptorGatewayTests
|
||||||
},
|
},
|
||||||
exposeInterceptorProtocol: true);
|
exposeInterceptorProtocol: true);
|
||||||
|
|
||||||
#pragma warning disable MCPEXP001
|
|
||||||
Assert.True(fixture.ProxyClient.ServerCapabilities.Extensions?.ContainsKey(InterceptorProtocolConstants.ExtensionCapabilityKey) ?? false);
|
Assert.True(fixture.ProxyClient.ServerCapabilities.Extensions?.ContainsKey(InterceptorProtocolConstants.ExtensionCapabilityKey) ?? false);
|
||||||
#pragma warning restore MCPEXP001
|
|
||||||
|
|
||||||
var tools = await fixture.ProxyClient.ListToolsAsync();
|
var tools = await fixture.ProxyClient.ListToolsAsync();
|
||||||
Assert.Single(tools);
|
Assert.Single(tools);
|
||||||
|
|
@ -934,6 +967,9 @@ public class McpInterceptorGatewayTests
|
||||||
Assert.Contains("alpha", result.Completion.Values);
|
Assert.Contains("alpha", result.Completion.Values);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Logging is deprecated upstream (SEP-2577) but the gateway intentionally keeps proxying it
|
||||||
|
// for down-level clients/backends.
|
||||||
|
#pragma warning disable MCP9005
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task SetLoggingLevelAsync_ProxiesToBackend()
|
public async Task SetLoggingLevelAsync_ProxiesToBackend()
|
||||||
{
|
{
|
||||||
|
|
@ -949,7 +985,10 @@ public class McpInterceptorGatewayTests
|
||||||
receivedLevel = request.Params!.Level;
|
receivedLevel = request.Params!.Level;
|
||||||
return new ValueTask<EmptyResult>(new EmptyResult());
|
return new ValueTask<EmptyResult>(new EmptyResult());
|
||||||
};
|
};
|
||||||
});
|
},
|
||||||
|
// logging/setLevel was replaced by per-request _meta log levels in spec 2026-07-28;
|
||||||
|
// the gateway keeps proxying it for down-level connections, so pin one here.
|
||||||
|
protocolVersion: "2025-06-18");
|
||||||
|
|
||||||
var caps = fixture.ProxyClient.ServerCapabilities;
|
var caps = fixture.ProxyClient.ServerCapabilities;
|
||||||
Assert.NotNull(caps?.Logging);
|
Assert.NotNull(caps?.Logging);
|
||||||
|
|
@ -957,6 +996,7 @@ public class McpInterceptorGatewayTests
|
||||||
await fixture.ProxyClient.SetLoggingLevelAsync(LoggingLevel.Warning);
|
await fixture.ProxyClient.SetLoggingLevelAsync(LoggingLevel.Warning);
|
||||||
Assert.Equal(LoggingLevel.Warning, receivedLevel);
|
Assert.Equal(LoggingLevel.Warning, receivedLevel);
|
||||||
}
|
}
|
||||||
|
#pragma warning restore MCP9005
|
||||||
|
|
||||||
// ── Test helpers ──────────────────────────────────────────────────
|
// ── Test helpers ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
@ -1042,7 +1082,8 @@ public class McpInterceptorGatewayTests
|
||||||
Action<McpServerOptions> backendConfigure,
|
Action<McpServerOptions> backendConfigure,
|
||||||
McpServerInterceptor[]? interceptors = null,
|
McpServerInterceptor[]? interceptors = null,
|
||||||
int? timeoutMs = null,
|
int? timeoutMs = null,
|
||||||
bool exposeInterceptorProtocol = false)
|
bool exposeInterceptorProtocol = false,
|
||||||
|
string? protocolVersion = null)
|
||||||
{
|
{
|
||||||
var disposables = new List<IAsyncDisposable>();
|
var disposables = new List<IAsyncDisposable>();
|
||||||
|
|
||||||
|
|
@ -1051,7 +1092,8 @@ public class McpInterceptorGatewayTests
|
||||||
// 1. Create backend server + client via pipes
|
// 1. Create backend server + client via pipes
|
||||||
var (backendServer, backendClient) = await CreateServerClientPair(
|
var (backendServer, backendClient) = await CreateServerClientPair(
|
||||||
"test-backend",
|
"test-backend",
|
||||||
backendConfigure);
|
backendConfigure,
|
||||||
|
protocolVersion: protocolVersion);
|
||||||
disposables.Add(backendServer);
|
disposables.Add(backendServer);
|
||||||
disposables.Add(backendClient);
|
disposables.Add(backendClient);
|
||||||
|
|
||||||
|
|
@ -1079,12 +1121,10 @@ public class McpInterceptorGatewayTests
|
||||||
options.Filters.Message.IncomingFilters.Add(filter.CreateFilter);
|
options.Filters.Message.IncomingFilters.Add(filter.CreateFilter);
|
||||||
|
|
||||||
options.Capabilities ??= new();
|
options.Capabilities ??= new();
|
||||||
#pragma warning disable MCPEXP001
|
|
||||||
options.Capabilities.Extensions ??= new Dictionary<string, object>();
|
options.Capabilities.Extensions ??= new Dictionary<string, object>();
|
||||||
options.Capabilities.Extensions[InterceptorProtocolConstants.ExtensionCapabilityKey] = JsonSerializer.SerializeToElement(
|
options.Capabilities.Extensions[InterceptorProtocolConstants.ExtensionCapabilityKey] = JsonSerializer.SerializeToElement(
|
||||||
new InterceptorsCapability { SupportedEvents = allEvents.ToList() },
|
new InterceptorsCapability { SupportedEvents = allEvents.ToList() },
|
||||||
InterceptorJsonUtilities.DefaultOptions);
|
InterceptorJsonUtilities.DefaultOptions);
|
||||||
#pragma warning restore MCPEXP001
|
|
||||||
});
|
});
|
||||||
disposables.Add(interceptorServer);
|
disposables.Add(interceptorServer);
|
||||||
disposables.Add(interceptorClient);
|
disposables.Add(interceptorClient);
|
||||||
|
|
@ -1105,7 +1145,8 @@ public class McpInterceptorGatewayTests
|
||||||
options =>
|
options =>
|
||||||
{
|
{
|
||||||
gateway.ConfigureServerOptions(options);
|
gateway.ConfigureServerOptions(options);
|
||||||
});
|
},
|
||||||
|
protocolVersion: protocolVersion);
|
||||||
disposables.Add(proxyServer);
|
disposables.Add(proxyServer);
|
||||||
disposables.Add(proxyClient);
|
disposables.Add(proxyClient);
|
||||||
|
|
||||||
|
|
@ -1159,12 +1200,10 @@ public class McpInterceptorGatewayTests
|
||||||
var filter = new InterceptorMessageFilter(collection);
|
var filter = new InterceptorMessageFilter(collection);
|
||||||
options.Filters.Message.IncomingFilters.Add(filter.CreateFilter);
|
options.Filters.Message.IncomingFilters.Add(filter.CreateFilter);
|
||||||
options.Capabilities ??= new();
|
options.Capabilities ??= new();
|
||||||
#pragma warning disable MCPEXP001
|
|
||||||
options.Capabilities.Extensions ??= new Dictionary<string, object>();
|
options.Capabilities.Extensions ??= new Dictionary<string, object>();
|
||||||
options.Capabilities.Extensions[InterceptorProtocolConstants.ExtensionCapabilityKey] = JsonSerializer.SerializeToElement(
|
options.Capabilities.Extensions[InterceptorProtocolConstants.ExtensionCapabilityKey] = JsonSerializer.SerializeToElement(
|
||||||
new InterceptorsCapability { SupportedEvents = allEvents.ToList() },
|
new InterceptorsCapability { SupportedEvents = allEvents.ToList() },
|
||||||
InterceptorJsonUtilities.DefaultOptions);
|
InterceptorJsonUtilities.DefaultOptions);
|
||||||
#pragma warning restore MCPEXP001
|
|
||||||
});
|
});
|
||||||
disposables.Add(server);
|
disposables.Add(server);
|
||||||
disposables.Add(client);
|
disposables.Add(client);
|
||||||
|
|
@ -1225,12 +1264,10 @@ public class McpInterceptorGatewayTests
|
||||||
options.Filters.Message.IncomingFilters.Add(filter.CreateFilter);
|
options.Filters.Message.IncomingFilters.Add(filter.CreateFilter);
|
||||||
|
|
||||||
options.Capabilities ??= new();
|
options.Capabilities ??= new();
|
||||||
#pragma warning disable MCPEXP001
|
|
||||||
options.Capabilities.Extensions ??= new Dictionary<string, object>();
|
options.Capabilities.Extensions ??= new Dictionary<string, object>();
|
||||||
options.Capabilities.Extensions[InterceptorProtocolConstants.ExtensionCapabilityKey] = JsonSerializer.SerializeToElement(
|
options.Capabilities.Extensions[InterceptorProtocolConstants.ExtensionCapabilityKey] = JsonSerializer.SerializeToElement(
|
||||||
new InterceptorsCapability { SupportedEvents = [] },
|
new InterceptorsCapability { SupportedEvents = [] },
|
||||||
InterceptorJsonUtilities.DefaultOptions);
|
InterceptorJsonUtilities.DefaultOptions);
|
||||||
#pragma warning restore MCPEXP001
|
|
||||||
});
|
});
|
||||||
disposables.Add(interceptorServer);
|
disposables.Add(interceptorServer);
|
||||||
disposables.Add(interceptorClient);
|
disposables.Add(interceptorClient);
|
||||||
|
|
@ -1302,12 +1339,10 @@ public class McpInterceptorGatewayTests
|
||||||
options.Filters.Message.IncomingFilters.Add(filter.CreateFilter);
|
options.Filters.Message.IncomingFilters.Add(filter.CreateFilter);
|
||||||
|
|
||||||
options.Capabilities ??= new();
|
options.Capabilities ??= new();
|
||||||
#pragma warning disable MCPEXP001
|
|
||||||
options.Capabilities.Extensions ??= new Dictionary<string, object>();
|
options.Capabilities.Extensions ??= new Dictionary<string, object>();
|
||||||
options.Capabilities.Extensions[InterceptorProtocolConstants.ExtensionCapabilityKey] = JsonSerializer.SerializeToElement(
|
options.Capabilities.Extensions[InterceptorProtocolConstants.ExtensionCapabilityKey] = JsonSerializer.SerializeToElement(
|
||||||
new InterceptorsCapability { SupportedEvents = [] },
|
new InterceptorsCapability { SupportedEvents = [] },
|
||||||
InterceptorJsonUtilities.DefaultOptions);
|
InterceptorJsonUtilities.DefaultOptions);
|
||||||
#pragma warning restore MCPEXP001
|
|
||||||
});
|
});
|
||||||
disposables.Add(interceptorServer);
|
disposables.Add(interceptorServer);
|
||||||
disposables.Add(interceptorClient);
|
disposables.Add(interceptorClient);
|
||||||
|
|
@ -1370,7 +1405,8 @@ public class McpInterceptorGatewayTests
|
||||||
private static async Task<(McpServer server, McpClient client)> CreateServerClientPair(
|
private static async Task<(McpServer server, McpClient client)> CreateServerClientPair(
|
||||||
string name,
|
string name,
|
||||||
Action<McpServerOptions> configure,
|
Action<McpServerOptions> configure,
|
||||||
Action<string>? onRegisterNotificationHandler = null)
|
Action<string>? onRegisterNotificationHandler = null,
|
||||||
|
string? protocolVersion = null)
|
||||||
{
|
{
|
||||||
// Create anonymous pipes for communication
|
// Create anonymous pipes for communication
|
||||||
var clientToServer = new AnonymousPipeServerStream(PipeDirection.Out);
|
var clientToServer = new AnonymousPipeServerStream(PipeDirection.Out);
|
||||||
|
|
@ -1394,7 +1430,13 @@ public class McpInterceptorGatewayTests
|
||||||
|
|
||||||
// Client writes to clientToServer, reads from clientFromServer
|
// Client writes to clientToServer, reads from clientFromServer
|
||||||
var clientTransport = new StreamClientTransport(clientToServer, clientFromServer);
|
var clientTransport = new StreamClientTransport(clientToServer, clientFromServer);
|
||||||
var client = await McpClient.CreateAsync(clientTransport);
|
var clientOptions = new McpClientOptions();
|
||||||
|
if (protocolVersion is not null)
|
||||||
|
{
|
||||||
|
clientOptions.ProtocolVersion = protocolVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
var client = await McpClient.CreateAsync(clientTransport, clientOptions);
|
||||||
|
|
||||||
return (server, client);
|
return (server, client);
|
||||||
}
|
}
|
||||||
|
|
@ -1437,6 +1479,8 @@ public class McpInterceptorGatewayTests
|
||||||
inner.SendMessageAsync(message, cancellationToken);
|
inner.SendMessageAsync(message, cancellationToken);
|
||||||
public override Task<JsonRpcResponse> SendRequestAsync(JsonRpcRequest request, CancellationToken cancellationToken = default) =>
|
public override Task<JsonRpcResponse> SendRequestAsync(JsonRpcRequest request, CancellationToken cancellationToken = default) =>
|
||||||
inner.SendRequestAsync(request, cancellationToken);
|
inner.SendRequestAsync(request, cancellationToken);
|
||||||
|
public override ValueTask<IDictionary<string, InputResponse>> ResolveInputRequestsAsync(IDictionary<string, InputRequest> inputRequests, CancellationToken cancellationToken) =>
|
||||||
|
inner.ResolveInputRequestsAsync(inputRequests, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed record NamedClient(string Name, McpClient Client);
|
private sealed record NamedClient(string Name, McpClient Client);
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||||
<PackageReference Include="xunit" Version="2.9.3" />
|
<PackageReference Include="xunit" Version="2.9.3" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||||
<PackageReference Include="ModelContextProtocol" Version="1.1.0" />
|
<PackageReference Include="ModelContextProtocol" Version="2.1.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue