-
Notifications
You must be signed in to change notification settings - Fork 672
Document recommended pattern for role/identity propagation in MCP tool execution #1497
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Copilot
wants to merge
3
commits into
main
Choose a base branch
from
copilot/update-documentation-role-identity-propagation
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+204
−1
Open
Changes from 2 commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,200 @@ | ||
| --- | ||
| title: Identity and Role Propagation | ||
| author: halter73 | ||
| description: How to access caller identity and roles in MCP tool, prompt, and resource handlers. | ||
| uid: identity | ||
| --- | ||
|
|
||
| # Identity and Role Propagation | ||
|
|
||
| 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. | ||
|
|
||
| ## How Identity Flows Through the SDK | ||
|
|
||
| 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. | ||
|
|
||
| ``` | ||
| HTTP Request (with auth token) | ||
| → ASP.NET Core Authentication Middleware (populates HttpContext.User) | ||
| → MCP Transport (copies User into JsonRpcMessage.Context.User) | ||
| → Message Filters (context.User available) | ||
| → Request Filters (context.User available) | ||
| → Tool / Prompt / Resource Handler (ClaimsPrincipal injected as parameter) | ||
| ``` | ||
|
|
||
| This means you can access the authenticated user's identity at every stage of request processing. | ||
|
|
||
| ## Direct `ClaimsPrincipal` Parameter Injection (Recommended) | ||
|
|
||
| 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: | ||
|
|
||
| ```csharp | ||
| [McpServerToolType] | ||
| public class UserAwareTools | ||
| { | ||
| [McpServerTool, Description("Returns a personalized greeting.")] | ||
| public string Greet(ClaimsPrincipal? user, string message) | ||
| { | ||
| var userName = user?.Identity?.Name ?? "anonymous"; | ||
| return $"{userName}: {message}"; | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| This pattern works the same way for prompts and resources: | ||
|
|
||
| ```csharp | ||
| [McpServerPromptType] | ||
| public class UserAwarePrompts | ||
| { | ||
| [McpServerPrompt, Description("Creates a user-specific prompt.")] | ||
| public ChatMessage PersonalizedPrompt(ClaimsPrincipal? user, string topic) | ||
| { | ||
| var userName = user?.Identity?.Name ?? "user"; | ||
| return new(ChatRole.User, $"As {userName}, explain {topic}."); | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ### Why This Works | ||
|
|
||
| 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: | ||
|
|
||
| 1. Excludes it from the generated JSON schema (clients never see it). | ||
| 2. Automatically resolves it from the current request's `User` property at invocation time. | ||
| 3. Passes `null` if no authenticated user is present (when the parameter is nullable). | ||
|
|
||
| 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. | ||
|
|
||
| ## Accessing Identity in Filters | ||
|
|
||
| Both message filters and request-specific filters expose the user via `context.User`: | ||
|
|
||
| ```csharp | ||
| services.AddMcpServer() | ||
| .WithRequestFilters(requestFilters => | ||
| { | ||
| requestFilters.AddCallToolFilter(next => async (context, cancellationToken) => | ||
| { | ||
| // Access user identity in a filter | ||
| var userName = context.User?.Identity?.Name; | ||
| var logger = context.Services?.GetService<ILogger<Program>>(); | ||
| logger?.LogInformation("Tool called by: {User}", userName ?? "anonymous"); | ||
|
|
||
| return await next(context, cancellationToken); | ||
| }); | ||
| }) | ||
| .WithTools<UserAwareTools>(); | ||
| ``` | ||
|
|
||
| ## Role-Based Access with `[Authorize]` Attributes | ||
|
|
||
| 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: | ||
|
|
||
| ```csharp | ||
| services.AddMcpServer() | ||
| .WithHttpTransport() | ||
| .AddAuthorizationFilters() | ||
| .WithTools<RoleProtectedTools>(); | ||
| ``` | ||
|
|
||
| Then decorate your tools with role requirements: | ||
|
|
||
| ```csharp | ||
| [McpServerToolType] | ||
| public class RoleProtectedTools | ||
| { | ||
| [McpServerTool, Description("Available to all authenticated users.")] | ||
| [Authorize] | ||
| public string GetData(string query) | ||
| { | ||
| return $"Data for: {query}"; | ||
| } | ||
|
|
||
| [McpServerTool, Description("Admin-only operation.")] | ||
| [Authorize(Roles = "Admin")] | ||
| public string AdminOperation(string action) | ||
| { | ||
| return $"Admin action: {action}"; | ||
| } | ||
|
|
||
| [McpServerTool, Description("Public tool accessible without authentication.")] | ||
| [AllowAnonymous] | ||
| public string PublicInfo() | ||
| { | ||
| return "This is public information."; | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| When authorization fails, the SDK automatically: | ||
|
|
||
| - **For list operations**: Removes unauthorized items from the results so users only see what they can access. | ||
| - **For individual operations**: Returns a JSON-RPC error indicating access is forbidden. | ||
|
|
||
| See [Filters](xref:filters) for more details on authorization filters and their execution order. | ||
|
|
||
| ## Using `IHttpContextAccessor` (HTTP-Only Alternative) | ||
|
|
||
| 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: | ||
|
|
||
| ```csharp | ||
| [McpServerToolType] | ||
| public class HttpContextTools(IHttpContextAccessor contextAccessor) | ||
| { | ||
| [McpServerTool, Description("Returns data filtered by caller identity.")] | ||
| public string GetFilteredData(string query) | ||
| { | ||
| var httpContext = contextAccessor.HttpContext | ||
| ?? throw new InvalidOperationException("No HTTP context available."); | ||
| var userName = httpContext.User.Identity?.Name ?? "anonymous"; | ||
| return $"{userName}: results for '{query}'"; | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| > [!IMPORTANT] | ||
| > `IHttpContextAccessor` only works with HTTP transports. For transport-agnostic identity access, use `ClaimsPrincipal` parameter injection instead. | ||
|
|
||
| See [HTTP Context](xref:httpcontext) for more details, including important caveats about stale `HttpContext` with the legacy SSE transport. | ||
|
|
||
| ## Transport Considerations | ||
|
|
||
| | Transport | Identity Source | Notes | | ||
| | --- | --- | --- | | ||
| | 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. | | ||
| | 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. | | ||
| | 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. | | ||
|
|
||
| ### Setting Identity for Stdio Transport | ||
|
|
||
| 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: | ||
|
|
||
| ```csharp | ||
| services.AddMcpServer() | ||
| .WithMessageFilters(messageFilters => | ||
| { | ||
| messageFilters.AddIncomingFilter(next => async (context, cancellationToken) => | ||
| { | ||
| // Set user based on process-level context | ||
| var role = Environment.GetEnvironmentVariable("MCP_USER_ROLE") ?? "default"; | ||
| context.User = new ClaimsPrincipal(new ClaimsIdentity( | ||
| [new Claim(ClaimTypes.Name, "stdio-user"), new Claim(ClaimTypes.Role, role)], | ||
| "StdioAuth", ClaimTypes.Name, ClaimTypes.Role)); | ||
|
|
||
| await next(context, cancellationToken); | ||
| }); | ||
| }) | ||
| .WithTools<UserAwareTools>(); | ||
| ``` | ||
|
|
||
| ## Full Example: Protected HTTP Server | ||
|
|
||
| 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). | ||
|
|
||
| The sample demonstrates: | ||
|
|
||
| - Configuring JWT Bearer authentication | ||
| - Setting up MCP authentication with resource metadata | ||
| - Using `RequireAuthorization()` to protect the MCP endpoint | ||
| - Implementing weather tools that require authentication |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.