diff --git a/docs/concepts/identity/identity.md b/docs/concepts/identity/identity.md new file mode 100644 index 000000000..3e9756b36 --- /dev/null +++ b/docs/concepts/identity/identity.md @@ -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>(); + logger?.LogInformation("Tool called by: {User}", userName ?? "anonymous"); + + return await next(context, cancellationToken); + }); + }) + .WithTools(); +``` + +## 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(); +``` + +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(); +``` + +## 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 diff --git a/docs/concepts/index.md b/docs/concepts/index.md index fc8f6edcc..6393d9997 100644 --- a/docs/concepts/index.md +++ b/docs/concepts/index.md @@ -40,3 +40,4 @@ Install the SDK and build your first MCP client and server. | [Stateless and Stateful](stateless/stateless.md) | Learn when to use stateless vs. stateful mode for HTTP servers and how to configure sessions. | | [HTTP Context](httpcontext/httpcontext.md) | Learn how to access the underlying `HttpContext` for a request. | | [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. | +| [Identity and Roles](identity/identity.md) | Learn how to access caller identity and roles in MCP tool, prompt, and resource handlers. | diff --git a/docs/concepts/toc.yml b/docs/concepts/toc.yml index d5d332b10..bd5474338 100644 --- a/docs/concepts/toc.yml +++ b/docs/concepts/toc.yml @@ -44,4 +44,6 @@ items: - name: HTTP Context uid: httpcontext - name: Filters - uid: filters \ No newline at end of file + uid: filters + - name: Identity and Roles + uid: identity \ No newline at end of file