Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
86356c9
Re-implementation
PederHP Mar 12, 2026
649b4b6
Sample 2
PederHP Mar 13, 2026
3e80b1d
Rename tool call from "add" to "get-sum" in GatewaySample
PederHP Mar 13, 2026
d1e1a18
Remaining InterceptingMcpClient methods, more samples, LLM event type
PederHP Mar 13, 2026
e4b1def
feat: Add transparent proxy sample and interceptor gateway
PederHP Apr 6, 2026
8153587
feat: Enhance interceptor gateway with multi-client support and notif…
PederHP Apr 6, 2026
7687cc7
feat: Update tests and enhance interceptor capabilities with result a…
PederHP Apr 6, 2026
558888a
feat: Improve notification forwarding registration to ensure it occur…
PederHP Apr 6, 2026
0cd8b3c
feat: Refactor interceptor chain handling to improve error reporting …
PederHP Apr 6, 2026
ee302c7
chore: Remove outdated CODE_REVIEW.md file to streamline documentation
PederHP Apr 6, 2026
d47de88
feat: Enhance InterceptingMcpClient and McpInterceptorGateway with im…
PederHP Apr 6, 2026
199fcf0
feat: Update documentation and tests for MCP interceptors, including …
PederHP Apr 6, 2026
150ad4f
feat: Add ExposeInterceptorProtocol option to McpInterceptorGateway f…
PederHP Apr 6, 2026
40c06b4
feat: Update README and tests to reflect backend capabilities mirrori…
PederHP Apr 6, 2026
9a5b5c3
feat(interceptors): Introduce Gateway Interceptor Protocol Bridge and…
PederHP Apr 6, 2026
3badc1d
feat(interceptors): Add service-provider-based configuration for inte…
PederHP Apr 6, 2026
3ba9ad8
feat(interceptors): Enhance McpInterceptorGateway to support external…
PederHP Apr 6, 2026
8b3f1eb
feat(tests): Add test to ensure backend tasks capability is not adver…
PederHP Apr 6, 2026
4e47239
feat(interceptors): Implement dynamic interceptor resolution and enha…
PederHP Apr 7, 2026
94d7716
feat(samples): Add Config-Driven Gateway Sample with JSON configurati…
PederHP Apr 7, 2026
4966bcb
feat(samples): Update Config-Driven Gateway Sample to support Streama…
PederHP Apr 7, 2026
8d3d7c3
docs: Update documentation
PederHP Apr 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
464 changes: 0 additions & 464 deletions csharp/sdk/AGENTS.md

This file was deleted.

509 changes: 72 additions & 437 deletions csharp/sdk/CLAUDE.md

Large diffs are not rendered by default.

183 changes: 96 additions & 87 deletions csharp/sdk/ModelContextProtocol.Interceptors.sln

Large diffs are not rendered by default.

302 changes: 229 additions & 73 deletions csharp/sdk/README.md
Original file line number Diff line number Diff line change
@@ -1,115 +1,271 @@
# MCP Interceptors C# SDK
# ModelContextProtocol.Interceptors

This library provides interceptor support for the Model Context Protocol (MCP) .NET SDK. Interceptors enable validation, mutation, and observation of MCP messages without modifying the original server or client implementations.
C# implementation of the [MCP Interceptors Extension (SEP-1763)](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1763) — gateway-level interceptors for the Model Context Protocol.

## Overview

MCP Interceptors (SEP-1763) allow you to:

- **Validate** incoming requests before they reach handlers
- **Mutate** requests or responses to transform data
- **Observe** message flow for logging, metrics, or auditing
Architecture work is tracked in [`docs/ARCHITECTURE_PHASES.md`](docs/ARCHITECTURE_PHASES.md).
SEP follow-up notes from the implementation are captured in [`docs/SEP_PROPOSAL_NOTES.md`](docs/SEP_PROPOSAL_NOTES.md).

Interceptors can be deployed as:
- Sidecars alongside MCP servers
- Gateway services that proxy MCP traffic
- Embedded validators within applications
## Overview

## Installation
This package enables creating interceptor servers that sit between MCP clients and servers, providing validation, mutation, and observability capabilities without modifying either the client or server.

```bash
dotnet add package ModelContextProtocol.Interceptors
```
Client ──▶ Interceptor Server ──▶ Server
◀── (validates/mutates) ◀── (tools)
```

## Quick Start

```csharp
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using ModelContextProtocol.Interceptors;
### Creating an Interceptor Server

```csharp
var builder = Host.CreateApplicationBuilder(args);

builder.Services.AddMcpServer()
.WithStdioServerTransport()
.WithInterceptors<ParameterValidator>();
.WithInterceptors<MyInterceptors>();

var app = builder.Build();
await app.RunAsync();

[McpServerInterceptorType]
public class MyInterceptors
{
[McpServerInterceptor(Name = "pii-validator", Type = InterceptorType.Validation,
Events = [InterceptorEvents.ToolsCall], Phase = InterceptorPhase.Request)]
public static ValidationInterceptorResult ValidatePii(JsonNode payload)
{
// Check for PII patterns
return ValidationInterceptorResult.Success();
}

await builder.Build().RunAsync();
[McpServerInterceptor(Name = "email-redactor", Type = InterceptorType.Mutation,
Events = [InterceptorEvents.ToolsCall], PriorityHint = -1000)]
public static MutationInterceptorResult RedactEmails(JsonNode payload)
{
// Modify the payload
return new MutationInterceptorResult { Modified = true, Payload = modifiedPayload };
}
}
```

## Creating an Interceptor
### Consuming Interceptors from a Client

```csharp
using ModelContextProtocol.Interceptors;
using ModelContextProtocol.Interceptors.Server;
using System.Text.Json.Nodes;
// Connect to the interceptor server
var interceptorClient = await McpClient.CreateAsync(interceptorTransport);

[McpServerInterceptorType]
public class ParameterValidator
// List available interceptors
var interceptors = await interceptorClient.ListInterceptorsAsync();

// Invoke a single interceptor
var result = await interceptorClient.InvokeInterceptorAsync(new InvokeInterceptorRequestParams
{
[McpServerInterceptor(
Name = "parameter-validator",
Description = "Validates tool call parameters",
Events = new[] { InterceptorEvents.ToolsCall },
Phase = InterceptorPhase.Request)]
public ValidationInterceptorResult ValidateToolCall(JsonNode? payload)
{
if (payload is null)
Name = "pii-validator",
Event = InterceptorEvents.ToolsCall,
Phase = InterceptorPhase.Request,
Payload = JsonNode.Parse("""{"name":"call-tool","arguments":{"query":"test"}}""")!,
});

// Execute a full chain
var chainResult = await interceptorClient.ExecuteChainAsync(new ExecuteChainRequestParams
{
Event = InterceptorEvents.ToolsCall,
Phase = InterceptorPhase.Request,
Payload = myPayload,
});
```

### Gateway Pattern (Client-Side)

Use `InterceptingMcpClient` when your code is the caller and you want to route operations through interceptors before they reach the server:

```csharp
// Connect to both the interceptor server and the actual MCP server
var interceptorClient = await McpClient.CreateAsync(interceptorTransport);
var mcpClient = await McpClient.CreateAsync(mcpTransport);

// Create the gateway wrapper
var gateway = new InterceptingMcpClient(mcpClient, new InterceptingMcpClientOptions
{
InterceptorClient = interceptorClient,
Events = [InterceptorEvents.ToolsCall],
});

// All tool calls now flow through interceptors automatically
var result = await gateway.CallToolAsync("my-tool", new Dictionary<string, object?> { ["query"] = "test" });
```

### Transparent Proxy (Server-Side)

Use `McpInterceptorGateway` to create an MCP server that transparently proxies requests through interceptors to a backend server. Connecting clients see the proxy as the backend itself — no client-side changes needed.

By default, the gateway is transparent-only: it does not advertise or expose the SEP interceptor protocol to connecting clients. If you want the gateway to also expose `interceptors/list`, `interceptor/invoke`, and `interceptor/executeChain`, enable that explicitly with `ExposeInterceptorProtocol = true`.

```
Client ──▶ Proxy Server ──▶ Interceptor Server ──▶ Backend Server
◀── (transparent) ◀── (validates/mutates) ◀── (tools, etc.)
```

```csharp
// Connect to the backend and interceptor servers
await using var backend = await McpClient.CreateAsync(backendTransport);
await using var interceptors = await McpClient.CreateAsync(interceptorTransport);

// Create the gateway
await using var gateway = new McpInterceptorGateway(new McpInterceptorGatewayOptions
{
BackendClient = backend,
InterceptorClients = [interceptors],
Events = [InterceptorEvents.ToolsCall], // null = intercept all events
ExposeInterceptorProtocol = false,
});

// Configure and start the proxy server on stdio
var serverOptions = new McpServerOptions();
gateway.ConfigureServerOptions(serverOptions);

await using var server = McpServer.Create(
new StdioServerTransport("my-proxy"), serverOptions);
gateway.RegisterNotificationForwarding(server);
await server.RunAsync();
```

The proxy mirrors the backend's advertised capability graph and forwards `*_list_changed` notifications for the supported list surfaces. Multiple interceptor clients can be chained — they execute in order, each receiving the previous client's mutated payload.

If you want the gateway to connect to external SEP-exposing interceptor servers itself, use the async factory and provide standard MCP client transports:

```csharp
await using var gateway = await McpInterceptorGateway.CreateAsync(new McpInterceptorGatewayOptions
{
BackendClient = backend,
InterceptorServerConnections =
[
new McpInterceptorServerConnectionOptions
{
return new ValidationInterceptorResult
Transport = new StdioClientTransport(new StdioClientTransportOptions
{
Valid = false,
Severity = ValidationSeverity.Error,
Messages = [new() { Message = "Payload is required" }]
};
Command = "dotnet",
Arguments = ["run", "--project", "path/to/interceptor-server"],
}),
},
],
});
```

This follows the same transport-driven pattern as `McpClient.CreateAsync(...)`: you supply `IClientTransport` instances such as `StdioClientTransport`, `HttpClientTransport`, or `StreamClientTransport`, along with optional `McpClientOptions` and logging via `McpInterceptorServerConnectionOptions`.

For dynamic transparent middleware scenarios, the gateway can also resolve external interceptor connections per request:

```csharp
await using var gateway = new McpInterceptorGateway(new McpInterceptorGatewayOptions
{
BackendClient = backend,
InterceptorClients = [staticSecurityLayer], // optional: resolver-only mode is also supported
InterceptorServerConnectionResolver = (context, @event, ct) =>
{
if (@event == InterceptorEvents.ToolsCall && context.User?.Identity?.Name == "alice")
{
return ValueTask.FromResult<IReadOnlyList<McpInterceptorServerConnectionOptions>>(
[
new McpInterceptorServerConnectionOptions
{
ConnectionId = "alice-logging",
Transport = new StdioClientTransport(new StdioClientTransportOptions
{
Command = "dotnet",
Arguments = ["run", "--project", "path/to/alice-interceptor-server"],
}),
},
]);
}

return new ValidationInterceptorResult { Valid = true };
}
}
return ValueTask.FromResult<IReadOnlyList<McpInterceptorServerConnectionOptions>>([]);
},
});
```

## Interceptor Types
The resolver receives the SDK's existing `MessageContext` plus the SEP event name. This lets you base resolution on existing request data such as `context.User`, `context.Items`, scoped services, or transport-specific information without introducing a separate gateway-specific identity abstraction.

### Validation Interceptors
Validate requests/responses and return pass/fail results with optional error messages.
In this first version, dynamic resolution is supported for the transparent proxy path only. SEP passthrough (`ExposeInterceptorProtocol = true`) still requires statically configured interceptor clients.

### Mutation Interceptors
Transform request or response payloads before they continue through the pipeline.
**With DI / builder pattern:**

### Observability Interceptors
Observe message flow for logging, metrics collection, or auditing without modifying data.
```csharp
builder.Services.AddMcpServer()
.WithInterceptorGateway(new McpInterceptorGatewayOptions
{
BackendClient = backend,
InterceptorClients = [interceptors],
});
```

The builder extension handles notification forwarding automatically, registering once per session for multi-connection transports (HTTP) and once for single-connection transports (stdio).

You can also configure the gateway from DI using a service-provider-based overload:

```csharp
builder.Services.AddMcpServer()
.WithInterceptorGateway(sp => new McpInterceptorGatewayOptions
{
BackendClient = sp.GetRequiredService<BackendMcpClientHolder>().Client,
InterceptorClients = [sp.GetRequiredService<InterceptorMcpClientHolder>().Client],
});
```

## Configuration Options
Internally, the builder path wires notification forwarding once per logical connection. Current transports may expose a session identifier, but that is treated as an implementation detail rather than a public architectural concept.

### Phase
- `InterceptorPhase.Request` - Intercept incoming requests
- `InterceptorPhase.Response` - Intercept outgoing responses
- `InterceptorPhase.Both` - Intercept both directions
The builder-based gateway configuration currently expects already-connected interceptor clients. External interceptor server connections that need async transport setup should use `McpInterceptorGateway.CreateAsync(...)`.

### Events
Interceptors can target specific MCP events:
- `InterceptorEvents.ToolsCall` - Tool invocation requests
- `InterceptorEvents.PromptGet` - Prompt retrieval
- `InterceptorEvents.ResourceRead` - Resource access
- And more...
If you want to see how these primitives can be composed into a config-driven host without the library defining a config format, see `samples/ConfigDrivenGatewaySample`. The sample shows transport-agnostic outbound connectivity for backend/interceptor servers (including Streamable HTTP). Its JSON schema is sample-only.

### Priority
Use `PriorityHint` to control interceptor execution order (lower values run first).
If you want to host the gateway itself over Streamable HTTP instead of stdio, compose `McpInterceptorGateway` with the core SDK's ASP.NET transport support (`AddMcpServer().WithHttpTransport()` and `MapMcp()`).

## Sample Projects
To expose the SEP interceptor protocol through the gateway as an advanced mode, set:

```csharp
ExposeInterceptorProtocol = true
```

**Claude Desktop integration** — point it at a proxy binary:

```json
{
"mcpServers": {
"my-server-with-interceptors": {
"command": "dotnet",
"args": ["run", "--project", "path/to/TransparentProxySample"]
}
}
}
```

## Interceptor Types

See the `samples/InterceptorServiceSample` directory for a complete example of a security-focused validation interceptor.
| Type | Execution | Purpose |
|------|-----------|---------|
| **Validation** | Parallel | Validates payloads. Error severity aborts the chain. |
| **Mutation** | Sequential (by priority) | Transforms payloads. Output chains to next mutation. |
| **Observability** | Parallel (fire-and-forget) | Logging/metrics. Failures are swallowed. |

## Requirements
## Chain Execution Order

- .NET 8.0 or later (or .NET Standard 2.0 compatible runtime)
- ModelContextProtocol SDK 0.1.0-preview.10 or later
**Request phase (sending):** Mutations → Validations → Observability → send
**Response phase (receiving):** Validations → Observability → Mutations → process

## License
## Parameter Binding

MIT License - see LICENSE file for details.
Interceptor methods support automatic parameter binding:

## Contributing
| Parameter Type | Bound From |
|---------------|------------|
| `JsonNode payload` | `InvokeInterceptorRequestParams.Payload` |
| `JsonNode config` | `InvokeInterceptorRequestParams.Config` |
| `string event` | `InvokeInterceptorRequestParams.Event` |
| `InterceptorPhase phase` | `InvokeInterceptorRequestParams.Phase` |
| `InvokeInterceptorContext` | `InvokeInterceptorRequestParams.Context` |
| `CancellationToken` | Framework cancellation token |
| `McpServer` | Current server instance |
| `IServiceProvider` | Request-scoped DI container |

Contributions are welcome! Please see the FSIG CONTRIBUTING.md for guidelines.
Methods can return `InterceptorResult` (or any subclass), `bool` (wrapped as `ValidationInterceptorResult`), or `Task<T>`/`ValueTask<T>` variants of these.
Loading
Loading