C# implementation of SEP-1763 --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
17 KiB
AGENTS.md - C# SDK for MCP Interceptors
This document provides guidance for AI coding agents working on the MCP Interceptors C# SDK implementation.
Note: See TODO.md for a list of code style alignment tasks with the MCP C# SDK.
Project Overview
This SDK implements SEP-1763: Interceptors for Model Context Protocol - a standardized framework for intercepting, validating, and transforming MCP messages.
Specification Reference: https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1763
Project Structure
csharp-sdk/
├── src/ModelContextProtocol.Interceptors/
│ ├── Protocol/ # Protocol types (Interceptor, Events, Results, etc.)
│ │ ├── InterceptorResult.cs # Abstract base class for all results
│ │ ├── ValidationInterceptorResult.cs
│ │ ├── MutationInterceptorResult.cs
│ │ ├── ObservabilityInterceptorResult.cs
│ │ ├── InterceptorChainResult.cs # Chain execution result
│ │ └── McpInterceptorValidationException.cs # Validation failure exception
│ ├── Server/ # Server-side implementation
│ │ ├── McpServerInterceptorAttribute.cs
│ │ ├── McpServerInterceptorTypeAttribute.cs
│ │ ├── McpServerInterceptor.cs # Abstract base class
│ │ ├── ReflectionMcpServerInterceptor.cs
│ │ ├── InterceptorServerHandlers.cs
│ │ └── InterceptorServerFilters.cs
│ ├── Client/ # Client-side implementation
│ │ ├── McpClientInterceptorAttribute.cs
│ │ ├── McpClientInterceptorTypeAttribute.cs
│ │ ├── McpClientInterceptor.cs # Abstract base class
│ │ ├── ReflectionMcpClientInterceptor.cs
│ │ ├── ClientInterceptorContext.cs
│ │ ├── McpClientInterceptorCreateOptions.cs
│ │ ├── InterceptorClientHandlers.cs
│ │ ├── InterceptorClientFilters.cs
│ │ ├── McpClientInterceptorExtensions.cs
│ │ ├── InterceptorChainExecutor.cs # Chain execution per SEP-1763
│ │ ├── InterceptingMcpClient.cs # McpClient wrapper with interceptors
│ │ ├── InterceptingMcpClientOptions.cs # Configuration for InterceptingMcpClient
│ │ ├── InterceptingMcpClientExtensions.cs # Extension methods
│ │ └── PayloadConverter.cs # JSON conversion utilities
│ └── McpServerInterceptorBuilderExtensions.cs # DI extensions
├── samples/
│ ├── InterceptorServiceSample/ # Server-side validation interceptor example
│ └── InterceptorClientSample/ # Client-side interceptor integration example
└── ModelContextProtocol.Interceptors.sln
Key Concepts from SEP-1763
Interceptor Types
- Validation - Validates requests/responses, returns pass/fail with severity levels
- Mutation - Transforms payloads before they continue through the pipeline
- Observability - Fire-and-forget logging/metrics collection, never blocks
Phases
Request- Intercept incoming requestsResponse- Intercept outgoing responsesBoth- Intercept in both directions
Events
Interceptors subscribe to specific MCP events:
- Server Features:
tools/list,tools/call,prompts/list,prompts/get,resources/list,resources/read,resources/subscribe - Client Features:
sampling/createMessage,elicitation/create,roots/list - LLM Interactions:
llm/completion - Wildcards:
*/request,*/response,*
Execution Order
Sending data (across trust boundary):
Mutate (sequential by priority) → Validate & Observe (parallel) → Send
Receiving data (from trust boundary):
Receive → Validate & Observe (parallel) → Mutate (sequential by priority)
Priority Ordering
- Mutations execute sequentially by
priorityHint(lower values first) - Ties broken alphabetically by interceptor name
- Validations and observability run in parallel (priority ignored)
- Recommended ranges: security (-2B to -1M), sanitization (-999K to -10K), normalization (-9999 to -1), default (0), enrichment (1-9999), observability (10K+)
Implementation Patterns
Creating a Server-Side Interceptor
[McpServerInterceptorType]
public class MyServerInterceptor
{
[McpServerInterceptor(
Name = "my-interceptor",
Description = "Description of what it does",
Events = new[] { InterceptorEvents.ToolsCall },
Phase = InterceptorPhase.Request,
PriorityHint = 0)]
public ValidationInterceptorResult Validate(JsonNode? payload)
{
// Implementation
return new ValidationInterceptorResult { Valid = true };
}
}
Creating a Client-Side Interceptor (Attribute-Based)
[McpClientInterceptorType]
public class MyClientInterceptors
{
[McpClientInterceptor(
Name = "pii-validator",
Description = "Validates tool arguments for PII leakage",
Events = new[] { InterceptorEvents.ToolsCall },
Phase = InterceptorPhase.Request,
PriorityHint = -1000)] // Security interceptors run early
public ValidationInterceptorResult ValidatePii(JsonNode? payload)
{
// Validate payload before sending to server
if (ContainsSsn(payload))
return ValidationInterceptorResult.Error("SSN detected in arguments");
return ValidationInterceptorResult.Success();
}
[McpClientInterceptor(
Name = "response-redactor",
Type = InterceptorType.Mutation,
Events = new[] { InterceptorEvents.ToolsCall },
Phase = InterceptorPhase.Response,
PriorityHint = 50)]
public MutationInterceptorResult RedactResponse(JsonNode? payload)
{
// Transform response received from server
var redacted = RedactSensitiveData(payload);
return MutationInterceptorResult.Mutated(redacted);
}
[McpClientInterceptor(
Name = "request-logger",
Type = InterceptorType.Observability,
Events = new[] { InterceptorEvents.ToolsCall },
Phase = InterceptorPhase.Request)]
public ObservabilityInterceptorResult LogRequest(JsonNode? payload, string @event)
{
Console.WriteLine($"Tool call: {@event}");
return ObservabilityInterceptorResult.Success();
}
}
Using InterceptingMcpClient (Full Integration)
The InterceptingMcpClient wraps McpClient and automatically executes interceptor chains for tool operations.
// Create MCP client normally
await using var client = await McpClient.CreateAsync(transport);
// Collect interceptors from attributed classes
var interceptors = new List<McpClientInterceptor>();
interceptors.AddRange(McpClientInterceptorExtensions.WithInterceptors<MyClientInterceptors>());
// Wrap with interceptors using extension method
var interceptedClient = client.WithInterceptors(new InterceptingMcpClientOptions
{
Interceptors = interceptors,
DefaultTimeoutMs = 5000,
ThrowOnValidationError = true, // Throw McpInterceptorValidationException on errors
InterceptResponses = true // Also intercept responses
});
// Use intercepted client - interceptors run automatically
try
{
var result = await interceptedClient.CallToolAsync("my-tool", new Dictionary<string, object?>
{
["name"] = "John Doe"
});
Console.WriteLine(result.Content?.FirstOrDefault());
}
catch (McpInterceptorValidationException ex)
{
Console.WriteLine($"Blocked by {ex.AbortedAt?.Interceptor}: {ex.AbortedAt?.Reason}");
Console.WriteLine(ex.GetDetailedMessage()); // Detailed validation info
}
// List tools (also intercepted)
var tools = await interceptedClient.ListToolsAsync();
// Access inner client for non-intercepted operations
var serverInfo = interceptedClient.ServerInfo;
Non-Throwing Mode
var interceptedClient = client.WithInterceptors(new InterceptingMcpClientOptions
{
Interceptors = interceptors,
ThrowOnValidationError = false // Return error result instead of throwing
});
var result = await interceptedClient.CallToolAsync("my-tool", args);
if (result.IsError)
{
// Handle validation failure from result
Console.WriteLine(result.Content?.FirstOrDefault());
}
Registration via DI (Server)
builder.Services.AddMcpServer()
.WithStdioServerTransport()
.WithInterceptors<MyServerInterceptor>();
Client Interceptor Chain Execution (Low-Level)
For advanced scenarios where you need direct chain execution:
// Create interceptors from attributed class
var interceptors = McpClientInterceptorExtensions.WithInterceptors<MyClientInterceptor>(services);
// Execute chain for outgoing requests
var executor = new InterceptorChainExecutor(interceptors, services);
var result = await executor.ExecuteForSendingAsync(
@event: InterceptorEvents.ToolsCall,
payload: requestPayload,
config: null,
timeoutMs: 5000);
if (result.Status == InterceptorChainStatus.Success)
{
// Use result.FinalPayload for the request
}
else if (result.Status == InterceptorChainStatus.ValidationFailed)
{
// Handle validation failure
Console.WriteLine($"Blocked by: {result.AbortedAt?.Interceptor}");
}
// Execute chain for incoming responses
var responseResult = await executor.ExecuteForReceivingAsync(
@event: InterceptorEvents.ToolsCall,
payload: responsePayload);
Validation Results
Return appropriate severity levels:
ValidationSeverity.Info- Informational, does not blockValidationSeverity.Warn- Warning, does not blockValidationSeverity.Error- Error, blocks execution
InterceptingMcpClient Architecture
The InterceptingMcpClient follows SEP-1763 execution order for tool operations:
User Application
│
▼
InterceptingMcpClient.CallToolAsync("my-tool", args)
│
├─► 1. PayloadConverter.ToCallToolRequestPayload(toolName, args)
│
├─► 2. _executor.ExecuteForSendingAsync("tools/call", payload)
│ │
│ ├── Mutations (sequential by priority)
│ ├── Validations (parallel)
│ └── Observability (fire-and-forget)
│
├─► 3. If ValidationFailed → throw McpInterceptorValidationException
│
├─► 4. PayloadConverter.FromCallToolRequestPayload(mutatedPayload)
│
├─► 5. _inner.CallToolAsync(mutatedParams)
│
├─► 6. PayloadConverter.ToCallToolResultPayload(result)
│
├─► 7. _executor.ExecuteForReceivingAsync("tools/call", responsePayload)
│
└─► 8. Return mutated result
Important: When using ListToolsAsync, the returned McpClientTool instances are associated with the inner McpClient, not the InterceptingMcpClient. Calling tool.InvokeAsync() will bypass interceptors. Use interceptedClient.CallToolAsync(toolName, args) directly to ensure interceptors execute.
Development Guidelines
When Adding New Protocol Types
- Follow the JSON-RPC patterns from the specification
- Place protocol types in
Protocol/directory - Use nullable reference types appropriately
- Add XML documentation comments
When Adding Server Features
- Add handler delegates in
Server/InterceptorServerHandlers.cs - Add filter delegates in
Server/InterceptorServerFilters.cs - Add builder extension methods in
McpServerInterceptorBuilderExtensions.cs - Ensure proper null checking and argument validation
When Adding Client Features
- Add handler delegates in
Client/InterceptorClientHandlers.cs - Add filter delegates in
Client/InterceptorClientFilters.cs - Add extension methods in
Client/McpClientInterceptorExtensions.cs - Update
InterceptorChainExecutorif chain execution logic changes - Ensure proper null checking and argument validation
Testing Considerations
- Test both valid and invalid payloads
- Test severity level propagation
- Test priority ordering for mutations
- Test parallel execution for validations
- Test fire-and-forget behavior for observability
- Test
McpInterceptorValidationExceptiondetails
Current Implementation Status
Implemented
Protocol Types:
InterceptorResult- Abstract base class with JSON polymorphism supportValidationInterceptorResult- For validation interceptorsMutationInterceptorResult- For mutation interceptorsObservabilityInterceptorResult- For observability interceptorsInterceptorChainResult- Result of chain executionMcpInterceptorValidationException- Exception for validation failures- Core types:
Interceptor,InterceptorEvent,InterceptorPhase,InterceptorType,InterceptorPriorityHint
Server-Side:
- Attribute-based interceptor registration (
McpServerInterceptor,McpServerInterceptorType) McpServerInterceptorabstract base classReflectionMcpServerInterceptorfor method-based interceptors- DI builder extensions
- Handler and filter delegates
Client-Side:
- Attribute-based interceptor registration (
McpClientInterceptor,McpClientInterceptorType) McpClientInterceptorabstract base classReflectionMcpClientInterceptorfor method-based interceptorsInterceptorChainExecutor- Executes interceptor chains per SEP-1763 spec- Extension methods for creating interceptors from types/assemblies
- Handler and filter delegates
InterceptingMcpClient- Full McpClient wrapper with automatic interceptor executionInterceptingMcpClientOptions- Configuration for the intercepting clientInterceptingMcpClientExtensions- Extension methods:client.WithInterceptors(options)PayloadConverter- JSON conversion utilities for request/response payloads
Samples:
InterceptorServiceSample- Server-side parameter validation interceptorInterceptorClientSample- Client-side interceptor integration demonstrating:- Validation interceptors (PII detection)
- Mutation interceptors (argument normalization, response redaction)
- Observability interceptors (request/response logging)
- Error handling with
McpInterceptorValidationException
Not Yet Implemented
Refer to SEP-1763 for full specification. Areas that may need work:
interceptor/executeChainprotocol method- Cryptographic signature verification (future feature)
- Unit tests for client-side chain execution
- LLM completion interceptors (
llm/completionevent)
Dependencies
ModelContextProtocolSDK (0.6.0-preview.10+)Microsoft.Extensions.DependencyInjectionMicrosoft.Extensions.HostingSystem.Text.Json
Target Frameworks
- .NET 10.0 (primary)
- .NET 9.0
- .NET 8.0
- .NET Standard 2.0 (for broader compatibility)
Common Tasks
Adding a New Event Type
- Add constant to
InterceptorEvents.cs - Update any event filtering logic
- Add tests for the new event
Adding a New Interceptor Type
- Add result type in
Protocol/(e.g.,MutationInterceptorResult.cs) - Update
InterceptorTypeenum if needed - Add handler support in server implementation
- Update builder extensions
Adding Support for New MCP Operations in InterceptingMcpClient
- Add payload conversion methods in
PayloadConverter.cs - Add the intercepted operation method in
InterceptingMcpClient.cs - Follow the existing pattern: convert → execute sending chain → call inner → execute receiving chain → return
Debugging Tips
- Check that interceptor methods are properly attributed
- Verify events match between registration and invocation
- Check priority values for mutation ordering issues
- Use logging to trace interceptor chain execution
- Inspect
McpInterceptorValidationException.ChainResultfor detailed failure info
Coding Style Guidelines
This project follows the coding style of the official MCP C# SDK.
Key Patterns
Null Validation:
// Use Throw helper instead of manual null checks
Throw.IfNull(parameter); // NOT: if (parameter is null) throw new ArgumentNullException(...)
Class Modifiers:
- Use
sealedon classes not designed for inheritance - Use
internalfor implementation details not part of the public API
Async/Await:
- Always use
ConfigureAwait(false)on awaits in library code - Prefer
ValueTaskfor hot paths to reduce allocations - Include
CancellationTokenon all async methods
JSON Serialization:
- Use
System.Text.Jsonexclusively - Use
JsonPropertyNameattributes for property mapping
Common Files
Common/Throw.cs- Helper class for null/argument validationCommon/Polyfills/- Compatibility attributes for netstandard2.0
Reference
For coding style examples, compare with:
/mnt/d/code/ai/mcp/csharp-sdk/src/ModelContextProtocol.Core//mnt/d/code/ai/mcp/csharp-sdk/src/Common/Throw.cs