Skip to content

Commit f0a98be

Browse files
author
Mike Kistler
authored
Merge branch 'main' into dependabot/npm_and_yarn/npm_and_yarn-f6ec07461d
2 parents 8616fb8 + a693ce2 commit f0a98be

26 files changed

Lines changed: 602 additions & 170 deletions

.github/copilot-instructions.md

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -85,22 +85,39 @@ The SDK consists of three main packages:
8585
- Test servers in `tests/ModelContextProtocol.Test*Server/` for integration scenarios
8686
- Filter manual tests with `[Trait("Execution", "Manual")]` - these require external dependencies
8787

88-
### Test Infrastructure and Helpers
89-
- **LoggedTest**: Base class for tests that need logging output captured to xUnit test output
90-
- Provides `ILoggerFactory` and `ITestOutputHelper` for test logging
91-
- Use when debugging or when tests need to verify log output
92-
- **TestServerTransport**: In-memory transport for testing client-server interactions without network I/O
93-
- **MockLoggerProvider**: For capturing and asserting on log messages
94-
- **XunitLoggerProvider**: Routes `ILogger` output to xUnit's `ITestOutputHelper`
95-
- **KestrelInMemoryTransport** (AspNetCore.Tests): In-memory Kestrel connection for HTTP transport testing without network stack
96-
97-
### Test Best Practices
98-
- Inherit from `LoggedTest` for tests needing logging infrastructure
99-
- Use `TestServerTransport` for in-memory client-server testing
100-
- Mock external dependencies (filesystem, HTTP clients) rather than calling real services
101-
- Use `CancellationTokenSource` with timeouts to prevent hanging tests
102-
- Dispose resources properly (servers, clients, transports) using `IDisposable` or `await using`
103-
- Run tests with: `dotnet test --filter '(Execution!=Manual)'`
88+
### Test Base Classes
89+
- **`LoggedTest`**: Base class that wires up `ILoggerFactory` with `XunitLoggerProvider` (test output) and `MockLoggerProvider` (log assertions). Inherit from this for any test needing logging.
90+
- **`ClientServerTestBase`**: Sets up in-memory client/server pair via `Pipe`. Override `ConfigureServices` to register tools/prompts/resources, then call `CreateMcpClientForServer()`. Handles async disposal automatically.
91+
- **`KestrelInMemoryTest`** (AspNetCore tests): Hosts ASP.NET Core with in-memory transport — no ports needed.
92+
- **`TestServerTransport`**: In-memory mock transport for testing client logic without a real server.
93+
94+
### Transport Selection in Tests
95+
- **Never use `WithStdioServerTransport()` in unit tests.** It reads from the test host's stdin, which cannot be closed, permanently leaking a thread pool thread per test.
96+
- For DI-only tests: `WithStreamServerTransport(Stream.Null, Stream.Null)`
97+
- For client/server interaction: inherit `ClientServerTestBase`
98+
- For client-only logic: use `TestServerTransport`
99+
- For HTTP/SSE: inherit `KestrelInMemoryTest`
100+
- For process lifecycle tests: `StdioClientTransport` (only when testing actual process behavior)
101+
102+
### Resource Management
103+
- **Always `await using` the `ServiceProvider`** when MCP server services are registered — `McpServerImpl` only implements `IAsyncDisposable`. Synchronous `using` throws at runtime.
104+
- **Always dispose clients and servers** — use `await using var client = ...`
105+
- **Use `TestContext.Current.CancellationToken`** for async MCP calls so xUnit can cancel on timeout.
106+
107+
### Timeouts
108+
- **Always use `TestConstants.DefaultTimeout`** (60s) instead of hardcoded values. CI machines are slower than dev workstations.
109+
- For HTTP polling operations use `TestConstants.HttpClientPollingTimeout` (2s).
110+
111+
### Synchronization
112+
- **Never use `Task.Delay` for synchronization.** Use `TaskCompletionSource`, `SemaphoreSlim`, or `Channel` so tests don't depend on timing.
113+
114+
### Background Logging
115+
- `ITestOutputHelper.WriteLine` throws after the test method returns. Background threads (process event handlers, async continuations) can outlive the test, causing unhandled exceptions that crash the test host.
116+
- Route logging through `LoggedTest.LoggerFactory``XunitLoggerProvider` already catches post-test exceptions.
117+
- If calling `ITestOutputHelper` directly from an event handler, wrap in try/catch for `InvalidOperationException`.
118+
119+
### Parallelism
120+
- Tests run in parallel by default. Apply `[Collection(nameof(DisableParallelization))]` to test classes that touch global state (e.g., `ActivitySource` listeners).
104121

105122
## Build and Development
106123

.github/workflows/codeql.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ jobs:
4747
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
4848
steps:
4949
- name: Checkout repository
50-
uses: actions/checkout@v4
50+
uses: actions/checkout@v6
5151

5252
# Add any setup steps before running the `github/codeql-action/init` action.
5353
# This includes steps like installing compilers or runtimes (`actions/setup-node`

CONTRIBUTING.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,70 @@ dotnet test tests/ModelContextProtocol.Tests/
6565

6666
Tools like Visual Studio, JetBrains Rider, and VS Code also provide integrated test runners that can be used to run and debug individual tests.
6767

68+
### Writing Tests
69+
70+
The test projects include shared infrastructure in `tests/Common/Utils/` that most tests build on. Familiarize yourself with these helpers before writing new tests.
71+
72+
#### Test base classes
73+
74+
- **`LoggedTest`** — Base class that wires up `ILoggerFactory` with both `XunitLoggerProvider` (routes to test output) and `MockLoggerProvider` (captures logs for assertions). Inherit from this for any test that needs logging.
75+
- **`ClientServerTestBase`** — Sets up an in-memory client/server pair connected via `Pipe` with proper async disposal. Override `ConfigureServices` to register tools, prompts, and resources, then call `CreateMcpClientForServer()` to get a connected client.
76+
- **`KestrelInMemoryTest`** (ASP.NET Core tests) — Hosts an ASP.NET Core server with in-memory transport so HTTP/SSE tests run without allocating ports.
77+
78+
#### Choosing a transport
79+
80+
| Scenario | Transport | Why |
81+
|---|---|---|
82+
| Unit tests that only need DI | `WithStreamServerTransport(Stream.Null, Stream.Null)` | No threads blocked, no process spawned |
83+
| Client/server interaction tests | `ClientServerTestBase` (uses `Pipe`) | Full bidirectional MCP, in-process |
84+
| Client-only logic | `TestServerTransport` | In-memory mock that auto-responds to standard MCP requests |
85+
| HTTP/SSE integration | `KestrelInMemoryTest` | Real HTTP stack, no network |
86+
| External process tests | `StdioClientTransport` | Only when testing actual process lifecycle |
87+
88+
> **Do not** use `WithStdioServerTransport()` in unit tests. The stdio server transport reads from the test host process's standard input, which the test does not own and cannot close. This means the transport's background read loop can never terminate, permanently leaking a thread pool thread per test. Use `WithStreamServerTransport(Stream.Null, Stream.Null)` for tests that only need the DI container.
89+
90+
#### Resource management
91+
92+
- **Always `await using` the `ServiceProvider`** when MCP server services are registered — `McpServerImpl` only implements `IAsyncDisposable`, not `IDisposable`. A synchronous `using` will throw at runtime, and skipping disposal leaks transports and background threads.
93+
- **Use `TestContext.Current.CancellationToken`** when calling async MCP methods so that xUnit can cancel the test on timeout rather than hanging.
94+
- **Dispose clients and servers** explicitly. Prefer `await using var client = ...` over relying on finalizers. `ClientServerTestBase` handles this if you inherit from it.
95+
96+
#### Timeouts
97+
98+
Use `TestConstants.DefaultTimeout` (60 seconds) rather than hardcoded values. CI machines are often slower than developer workstations, and short timeouts cause flaky failures.
99+
100+
```csharp
101+
// Good
102+
using var cts = CancellationTokenSource.CreateLinkedTokenSource(TestContext.Current.CancellationToken);
103+
cts.CancelAfter(TestConstants.DefaultTimeout);
104+
await client.CallToolAsync("my-tool", args, cts.Token);
105+
106+
// Bad — too short for CI
107+
cts.CancelAfter(TimeSpan.FromSeconds(5));
108+
```
109+
110+
#### Synchronization
111+
112+
Avoid `Task.Delay` for synchronization. Use explicit signaling primitives (`TaskCompletionSource`, `SemaphoreSlim`, `Channel`) so tests don't depend on timing. If a producer/consumer test writes events for a streaming reader, use a `TaskCompletionSource` to confirm the reader is active before writing.
113+
114+
#### Background logging
115+
116+
`ITestOutputHelper.WriteLine` throws after the test method returns. Background threads (process event handlers, async continuations) can outlive the test. This manifests as unhandled exceptions that crash the test host. Two mitigations:
117+
118+
1. **`XunitLoggerProvider`** already catches these exceptions. Route logging through `LoggedTest.LoggerFactory` rather than calling `ITestOutputHelper` directly from callbacks.
119+
2. **If you must call `ITestOutputHelper` from an event handler**, wrap it in a try/catch:
120+
```csharp
121+
process.ErrorDataReceived += (s, e) =>
122+
{
123+
try { testOutputHelper.WriteLine(e.Data); }
124+
catch (InvalidOperationException) { }
125+
};
126+
```
127+
128+
#### Parallelism
129+
130+
Tests run in parallel by default. If a test class touches global state (e.g., `ActivitySource` listeners in diagnostics tests), apply `[Collection(nameof(DisableParallelization))]` to run it sequentially.
131+
68132
### Building the Documentation
69133

70134
This project uses [DocFX](https://dotnet.github.io/docfx/) to generate its conceptual and reference documentation.

Directory.Packages.props

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,11 +76,11 @@
7676
<PackageVersion Include="Microsoft.Extensions.TimeProvider.Testing" Version="10.1.0" />
7777
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.3.0" />
7878
<PackageVersion Include="Moq" Version="4.20.72" />
79-
<PackageVersion Include="OpenTelemetry" Version="1.15.1" />
80-
<PackageVersion Include="OpenTelemetry.Exporter.InMemory" Version="1.15.1" />
81-
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.15.1" />
79+
<PackageVersion Include="OpenTelemetry" Version="1.15.2" />
80+
<PackageVersion Include="OpenTelemetry.Exporter.InMemory" Version="1.15.2" />
81+
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.15.2" />
8282
<PackageVersion Include="OpenTelemetry.Instrumentation.Http" Version="1.15.0" />
83-
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="1.15.1" />
83+
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="1.15.2" />
8484
<PackageVersion Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.15.1" />
8585
<PackageVersion Include="Serilog.Extensions.Hosting" Version="10.0.0" />
8686
<PackageVersion Include="Serilog.Extensions.Logging" Version="10.0.0" />

docs/concepts/identity/identity.md

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
---
2+
title: Identity and Role Propagation
3+
author: halter73
4+
description: How to access caller identity and roles in MCP tool, prompt, and resource handlers.
5+
uid: identity
6+
---
7+
8+
# Identity and Role Propagation
9+
10+
When building production MCP servers, you often need to know _who_ is calling a tool so you can enforce permissions, filter data, or audit access. The MCP C# SDK provides built-in support for propagating the caller's identity from the transport layer into your tool, prompt, and resource handlers — no custom headers or workarounds required.
11+
12+
## How Identity Flows Through the SDK
13+
14+
When a client sends a request over an authenticated HTTP transport (Streamable HTTP or SSE), the ASP.NET Core authentication middleware populates `HttpContext.User` with a `ClaimsPrincipal`. The SDK's transport layer automatically copies this `ClaimsPrincipal` into `JsonRpcMessage.Context.User`, which then flows through message filters, request filters, and finally into the handler or tool method.
15+
16+
```
17+
HTTP Request (with auth token)
18+
→ ASP.NET Core Authentication Middleware (populates HttpContext.User)
19+
→ MCP Transport (copies User into JsonRpcMessage.Context.User)
20+
→ Message Filters (context.User available)
21+
→ Request Filters (context.User available)
22+
→ Tool / Prompt / Resource Handler (ClaimsPrincipal injected as parameter)
23+
```
24+
25+
This means you can access the authenticated user's identity at every stage of request processing.
26+
27+
## Direct `ClaimsPrincipal` Parameter Injection (Recommended)
28+
29+
The simplest and recommended approach is to declare a `ClaimsPrincipal` parameter on your tool method. The SDK automatically injects the authenticated user without including it in the tool's input schema:
30+
31+
```csharp
32+
[McpServerToolType]
33+
public class UserAwareTools
34+
{
35+
[McpServerTool, Description("Returns a personalized greeting.")]
36+
public string Greet(ClaimsPrincipal? user, string message)
37+
{
38+
var userName = user?.Identity?.Name ?? "anonymous";
39+
return $"{userName}: {message}";
40+
}
41+
}
42+
```
43+
44+
This pattern works the same way for prompts and resources:
45+
46+
```csharp
47+
[McpServerPromptType]
48+
public class UserAwarePrompts
49+
{
50+
[McpServerPrompt, Description("Creates a user-specific prompt.")]
51+
public ChatMessage PersonalizedPrompt(ClaimsPrincipal? user, string topic)
52+
{
53+
var userName = user?.Identity?.Name ?? "user";
54+
return new(ChatRole.User, $"As {userName}, explain {topic}.");
55+
}
56+
}
57+
```
58+
59+
### Why This Works
60+
61+
The SDK registers `ClaimsPrincipal` as one of the built-in services available during request processing. When a tool, prompt, or resource method declares a `ClaimsPrincipal` parameter, the SDK:
62+
63+
1. Excludes it from the generated JSON schema (clients never see it).
64+
2. Automatically resolves it from the current request's `User` property at invocation time.
65+
3. Passes `null` if no authenticated user is present (when the parameter is nullable).
66+
67+
This behavior is transport-agnostic. For HTTP transports, the `ClaimsPrincipal` comes from ASP.NET Core authentication. For other transports (like stdio), it will be `null` unless you set it explicitly via a message filter.
68+
69+
## Accessing Identity in Filters
70+
71+
Both message filters and request-specific filters expose the user via `context.User`:
72+
73+
```csharp
74+
services.AddMcpServer()
75+
.WithRequestFilters(requestFilters =>
76+
{
77+
requestFilters.AddCallToolFilter(next => async (context, cancellationToken) =>
78+
{
79+
// Access user identity in a filter
80+
var userName = context.User?.Identity?.Name;
81+
var logger = context.Services?.GetService<ILogger<Program>>();
82+
logger?.LogInformation("Tool called by: {User}", userName ?? "anonymous");
83+
84+
return await next(context, cancellationToken);
85+
});
86+
})
87+
.WithTools<UserAwareTools>();
88+
```
89+
90+
## Role-Based Access with `[Authorize]` Attributes
91+
92+
For declarative authorization, you can use standard ASP.NET Core `[Authorize]` attributes on your tools, prompts, and resources. This requires calling `AddAuthorizationFilters()` during server configuration:
93+
94+
```csharp
95+
services.AddMcpServer()
96+
.WithHttpTransport()
97+
.AddAuthorizationFilters()
98+
.WithTools<RoleProtectedTools>();
99+
```
100+
101+
Then decorate your tools with role requirements:
102+
103+
```csharp
104+
[McpServerToolType]
105+
public class RoleProtectedTools
106+
{
107+
[McpServerTool, Description("Available to all authenticated users.")]
108+
[Authorize]
109+
public string GetData(string query)
110+
{
111+
return $"Data for: {query}";
112+
}
113+
114+
[McpServerTool, Description("Admin-only operation.")]
115+
[Authorize(Roles = "Admin")]
116+
public string AdminOperation(string action)
117+
{
118+
return $"Admin action: {action}";
119+
}
120+
121+
[McpServerTool, Description("Public tool accessible without authentication.")]
122+
[AllowAnonymous]
123+
public string PublicInfo()
124+
{
125+
return "This is public information.";
126+
}
127+
}
128+
```
129+
130+
When authorization fails, the SDK automatically:
131+
132+
- **For list operations**: Removes unauthorized items from the results so users only see what they can access.
133+
- **For individual operations**: Returns a JSON-RPC error indicating access is forbidden.
134+
135+
See [Filters](xref:filters) for more details on authorization filters and their execution order.
136+
137+
## Using `IHttpContextAccessor` (HTTP-Only Alternative)
138+
139+
If you need access to the full `HttpContext` (not just the user), you can inject `IHttpContextAccessor` into your tool class. This gives you access to HTTP headers, query strings, and other request metadata:
140+
141+
```csharp
142+
[McpServerToolType]
143+
public class HttpContextTools(IHttpContextAccessor contextAccessor)
144+
{
145+
[McpServerTool, Description("Returns data filtered by caller identity.")]
146+
public string GetFilteredData(string query)
147+
{
148+
var httpContext = contextAccessor.HttpContext
149+
?? throw new InvalidOperationException("No HTTP context available.");
150+
var userName = httpContext.User.Identity?.Name ?? "anonymous";
151+
return $"{userName}: results for '{query}'";
152+
}
153+
}
154+
```
155+
156+
> [!IMPORTANT]
157+
> `IHttpContextAccessor` only works with HTTP transports. For transport-agnostic identity access, use `ClaimsPrincipal` parameter injection instead.
158+
159+
See [HTTP Context](xref:httpcontext) for more details, including important caveats about stale `HttpContext` with the legacy SSE transport.
160+
161+
## Transport Considerations
162+
163+
| Transport | Identity Source | Notes |
164+
| --- | --- | --- |
165+
| Streamable HTTP | ASP.NET Core authentication middleware populates `HttpContext.User`, which the transport copies to each request. | Recommended for production. Each request carries fresh authentication context. |
166+
| SSE | Same as Streamable HTTP, but the `HttpContext` is tied to the long-lived SSE connection. | The `ClaimsPrincipal` parameter injection still works correctly, but `IHttpContextAccessor` may return stale claims if the client's token was refreshed after the SSE connection was established. |
167+
| Stdio | No built-in authentication. `ClaimsPrincipal` is `null` unless set via a message filter. | For process-level identity, you can set the user in a message filter based on environment variables or other process-level context. |
168+
169+
### Setting Identity for Stdio Transport
170+
171+
For stdio-based servers where the caller's identity comes from the process environment rather than HTTP authentication, you can set the user in a message filter:
172+
173+
```csharp
174+
services.AddMcpServer()
175+
.WithMessageFilters(messageFilters =>
176+
{
177+
messageFilters.AddIncomingFilter(next => async (context, cancellationToken) =>
178+
{
179+
// Set user based on process-level context
180+
var role = Environment.GetEnvironmentVariable("MCP_USER_ROLE") ?? "default";
181+
context.User = new ClaimsPrincipal(new ClaimsIdentity(
182+
[new Claim(ClaimTypes.Name, "stdio-user"), new Claim(ClaimTypes.Role, role)],
183+
"StdioAuth", ClaimTypes.Name, ClaimTypes.Role));
184+
185+
await next(context, cancellationToken);
186+
});
187+
})
188+
.WithTools<UserAwareTools>();
189+
```
190+
191+
## Full Example: Protected HTTP Server
192+
193+
For a complete example of an MCP server with JWT authentication, OAuth resource metadata, and protected tools, see the [ProtectedMcpServer sample](https://github.com/modelcontextprotocol/csharp-sdk/tree/main/samples/ProtectedMcpServer).
194+
195+
The sample demonstrates:
196+
197+
- Configuring JWT Bearer authentication
198+
- Setting up MCP authentication with resource metadata
199+
- Using `RequireAuthorization()` to protect the MCP endpoint
200+
- Implementing weather tools that require authentication

docs/concepts/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,4 @@ Install the SDK and build your first MCP client and server.
4040
| [Stateless and Stateful](stateless/stateless.md) | Learn when to use stateless vs. stateful mode for HTTP servers and how to configure sessions. |
4141
| [HTTP Context](httpcontext/httpcontext.md) | Learn how to access the underlying `HttpContext` for a request. |
4242
| [MCP Server Handler Filters](filters.md) | Learn how to add filters to the handler pipeline. Filters let you wrap the original handler with additional functionality. |
43+
| [Identity and Roles](identity/identity.md) | Learn how to access caller identity and roles in MCP tool, prompt, and resource handlers. |

docs/concepts/toc.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,6 @@ items:
4444
- name: HTTP Context
4545
uid: httpcontext
4646
- name: Filters
47-
uid: filters
47+
uid: filters
48+
- name: Identity and Roles
49+
uid: identity

samples/AspNetCoreMcpPerSessionTools/AspNetCoreMcpPerSessionTools.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
</ItemGroup>
1313

1414
<ItemGroup>
15+
<PackageReference Include="OpenTelemetry" />
1516
<PackageReference Include="OpenTelemetry.Extensions.Hosting" />
1617
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" />
1718
<PackageReference Include="OpenTelemetry.Instrumentation.Http" />

0 commit comments

Comments
 (0)