Add middleware and authz support for server-side handlers (#733)
This commit is contained in:
parent
0f0aad020b
commit
d344c65120
59 changed files with 2871 additions and 435 deletions
317
docs/concepts/filters.md
Normal file
317
docs/concepts/filters.md
Normal file
|
|
@ -0,0 +1,317 @@
|
|||
---
|
||||
title: Filters
|
||||
author: halter73
|
||||
description: MCP Server Handler Filters
|
||||
uid: filters
|
||||
---
|
||||
|
||||
# MCP Server Handler Filters
|
||||
|
||||
For each handler type in the MCP Server, there are corresponding `AddXXXFilter` methods in `McpServerBuilderExtensions.cs` that allow you to add filters to the handler pipeline. The filters are stored in `McpServerOptions.Filters` and applied during server configuration.
|
||||
|
||||
## Available Filter Methods
|
||||
|
||||
The following filter methods are available:
|
||||
|
||||
- `AddListResourceTemplatesFilter` - Filter for list resource templates handlers
|
||||
- `AddListToolsFilter` - Filter for list tools handlers
|
||||
- `AddCallToolFilter` - Filter for call tool handlers
|
||||
- `AddListPromptsFilter` - Filter for list prompts handlers
|
||||
- `AddGetPromptFilter` - Filter for get prompt handlers
|
||||
- `AddListResourcesFilter` - Filter for list resources handlers
|
||||
- `AddReadResourceFilter` - Filter for read resource handlers
|
||||
- `AddCompleteFilter` - Filter for completion handlers
|
||||
- `AddSubscribeToResourcesFilter` - Filter for resource subscription handlers
|
||||
- `AddUnsubscribeFromResourcesFilter` - Filter for resource unsubscription handlers
|
||||
- `AddSetLoggingLevelFilter` - Filter for logging level handlers
|
||||
|
||||
## Usage
|
||||
|
||||
Filters are functions that take a handler and return a new handler, allowing you to wrap the original handler with additional functionality:
|
||||
|
||||
```csharp
|
||||
services.AddMcpServer()
|
||||
.WithListToolsHandler(async (context, cancellationToken) =>
|
||||
{
|
||||
// Your base handler logic
|
||||
return new ListToolsResult { Tools = GetTools() };
|
||||
})
|
||||
.AddListToolsFilter(next => async (context, cancellationToken) =>
|
||||
{
|
||||
var logger = context.Services?.GetService<ILogger<Program>>();
|
||||
|
||||
// Pre-processing logic
|
||||
logger?.LogInformation("Before handler execution");
|
||||
|
||||
var result = await next(context, cancellationToken);
|
||||
|
||||
// Post-processing logic
|
||||
logger?.LogInformation("After handler execution");
|
||||
return result;
|
||||
});
|
||||
```
|
||||
|
||||
## Filter Execution Order
|
||||
|
||||
```csharp
|
||||
services.AddMcpServer()
|
||||
.WithListToolsHandler(baseHandler)
|
||||
.AddListToolsFilter(filter1) // Executes first (outermost)
|
||||
.AddListToolsFilter(filter2) // Executes second
|
||||
.AddListToolsFilter(filter3); // Executes third (closest to handler)
|
||||
```
|
||||
|
||||
Execution flow: `filter1 -> filter2 -> filter3 -> baseHandler -> filter3 -> filter2 -> filter1`
|
||||
|
||||
## Common Use Cases
|
||||
|
||||
### Logging
|
||||
```csharp
|
||||
.AddListToolsFilter(next => async (context, cancellationToken) =>
|
||||
{
|
||||
var logger = context.Services?.GetService<ILogger<Program>>();
|
||||
|
||||
logger?.LogInformation($"Processing request from {context.Meta.ProgressToken}");
|
||||
var result = await next(context, cancellationToken);
|
||||
logger?.LogInformation($"Returning {result.Tools?.Count ?? 0} tools");
|
||||
return result;
|
||||
});
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
```csharp
|
||||
.AddCallToolFilter(next => async (context, cancellationToken) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
return await next(context, cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new CallToolResult
|
||||
{
|
||||
Content = new[] { new TextContent { Type = "text", Text = $"Error: {ex.Message}" } },
|
||||
IsError = true
|
||||
};
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Performance Monitoring
|
||||
```csharp
|
||||
.AddListToolsFilter(next => async (context, cancellationToken) =>
|
||||
{
|
||||
var logger = context.Services?.GetService<ILogger<Program>>();
|
||||
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
var result = await next(context, cancellationToken);
|
||||
stopwatch.Stop();
|
||||
logger?.LogInformation($"Handler took {stopwatch.ElapsedMilliseconds}ms");
|
||||
return result;
|
||||
});
|
||||
```
|
||||
|
||||
### Caching
|
||||
```csharp
|
||||
.AddListResourcesFilter(next => async (context, cancellationToken) =>
|
||||
{
|
||||
var cache = context.Services!.GetRequiredService<IMemoryCache>();
|
||||
|
||||
var cacheKey = $"resources:{context.Params.Cursor}";
|
||||
if (cache.TryGetValue(cacheKey, out var cached))
|
||||
{
|
||||
return (ListResourcesResult)cached;
|
||||
}
|
||||
|
||||
var result = await next(context, cancellationToken);
|
||||
cache.Set(cacheKey, result, TimeSpan.FromMinutes(5));
|
||||
return result;
|
||||
});
|
||||
```
|
||||
|
||||
## Built-in Authorization Filters
|
||||
|
||||
When using the ASP.NET Core integration (`ModelContextProtocol.AspNetCore`), you can add authorization filters to support `[Authorize]` and `[AllowAnonymous]` attributes on MCP server tools, prompts, and resources by calling `AddAuthorizationFilters()` on your MCP server builder.
|
||||
|
||||
### Enabling Authorization Filters
|
||||
|
||||
To enable authorization support, call `AddAuthorizationFilters()` when configuring your MCP server:
|
||||
|
||||
```csharp
|
||||
services.AddMcpServer()
|
||||
.WithHttpTransport()
|
||||
.AddAuthorizationFilters() // Enable authorization filter support
|
||||
.WithTools<WeatherTools>();
|
||||
```
|
||||
|
||||
**Important**: You should always call `AddAuthorizationFilters()` when using ASP.NET Core integration if you want to use authorization attributes like `[Authorize]` on your MCP server tools, prompts, or resources.
|
||||
|
||||
### Authorization Attributes Support
|
||||
|
||||
The MCP server automatically respects the following authorization attributes:
|
||||
|
||||
- **`[Authorize]`** - Requires authentication for access
|
||||
- **`[Authorize(Roles = "RoleName")]`** - Requires specific roles
|
||||
- **`[Authorize(Policy = "PolicyName")]`** - Requires specific authorization policies
|
||||
- **`[AllowAnonymous]`** - Explicitly allows anonymous access (overrides `[Authorize]`)
|
||||
|
||||
### Tool Authorization
|
||||
|
||||
Tools can be decorated with authorization attributes to control access:
|
||||
|
||||
```csharp
|
||||
[McpServerToolType]
|
||||
public class WeatherTools
|
||||
{
|
||||
[McpServerTool, Description("Gets public weather data")]
|
||||
public static string GetWeather(string location)
|
||||
{
|
||||
return $"Weather for {location}: Sunny, 25°C";
|
||||
}
|
||||
|
||||
[McpServerTool, Description("Gets detailed weather forecast")]
|
||||
[Authorize] // Requires authentication
|
||||
public static string GetDetailedForecast(string location)
|
||||
{
|
||||
return $"Detailed forecast for {location}: ...";
|
||||
}
|
||||
|
||||
[McpServerTool, Description("Manages weather alerts")]
|
||||
[Authorize(Roles = "Admin")] // Requires Admin role
|
||||
public static string ManageWeatherAlerts(string alertType)
|
||||
{
|
||||
return $"Managing alert: {alertType}";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Class-Level Authorization
|
||||
|
||||
You can apply authorization at the class level, which affects all tools in the class:
|
||||
|
||||
```csharp
|
||||
[McpServerToolType]
|
||||
[Authorize] // All tools require authentication
|
||||
public class RestrictedTools
|
||||
{
|
||||
[McpServerTool, Description("Restricted tool accessible to authenticated users")]
|
||||
public static string RestrictedOperation()
|
||||
{
|
||||
return "Restricted operation completed";
|
||||
}
|
||||
|
||||
[McpServerTool, Description("Public tool accessible to anonymous users")]
|
||||
[AllowAnonymous] // Overrides class-level [Authorize]
|
||||
public static string PublicOperation()
|
||||
{
|
||||
return "Public operation completed";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### How Authorization Filters Work
|
||||
|
||||
The authorization filters work differently for list operations versus individual operations:
|
||||
|
||||
#### List Operations (ListTools, ListPrompts, ListResources)
|
||||
For list operations, the filters automatically remove unauthorized items from the results. Users only see tools, prompts, or resources they have permission to access.
|
||||
|
||||
#### Individual Operations (CallTool, GetPrompt, ReadResource)
|
||||
For individual operations, the filters throw an `McpException` with "Access forbidden" message. These get turned into JSON-RPC errors if uncaught by middleware.
|
||||
|
||||
### Filter Execution Order and Authorization
|
||||
|
||||
Authorization filters are applied automatically when you call `AddAuthorizationFilters()`. These filters run at a specific point in the filter pipeline, which means:
|
||||
|
||||
**Filters added before authorization filters** can see:
|
||||
- Unauthorized requests for operations before they are rejected by the authorization filters
|
||||
- Complete listings for unauthorized primitives before they are filtered out by the authorization filters
|
||||
|
||||
**Filters added after authorization filters** will only see:
|
||||
- Authorized requests that passed authorization checks
|
||||
- Filtered listings containing only authorized primitives
|
||||
|
||||
This allows you to implement logging, metrics, or other cross-cutting concerns that need to see all requests, while still maintaining proper authorization:
|
||||
|
||||
```csharp
|
||||
services.AddMcpServer()
|
||||
.WithHttpTransport()
|
||||
.AddListToolsFilter(next => async (context, cancellationToken) =>
|
||||
{
|
||||
var logger = context.Services?.GetService<ILogger<Program>>();
|
||||
|
||||
// This filter runs BEFORE authorization - sees all tools
|
||||
logger?.LogInformation("Request for tools list - will see all tools");
|
||||
var result = await next(context, cancellationToken);
|
||||
logger?.LogInformation($"Returning {result.Tools?.Count ?? 0} tools after authorization");
|
||||
return result;
|
||||
})
|
||||
.AddAuthorizationFilters() // Authorization filtering happens here
|
||||
.AddListToolsFilter(next => async (context, cancellationToken) =>
|
||||
{
|
||||
var logger = context.Services?.GetService<ILogger<Program>>();
|
||||
|
||||
// This filter runs AFTER authorization - only sees authorized tools
|
||||
var result = await next(context, cancellationToken);
|
||||
logger?.LogInformation($"Post-auth filter sees {result.Tools?.Count ?? 0} authorized tools");
|
||||
return result;
|
||||
})
|
||||
.WithTools<WeatherTools>();
|
||||
```
|
||||
|
||||
### Setup Requirements
|
||||
|
||||
To use authorization features, you must configure authentication and authorization in your ASP.NET Core application and call `AddAuthorizationFilters()`:
|
||||
|
||||
```csharp
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Services.AddAuthentication("Bearer")
|
||||
.AddJwtBearer(options => { /* JWT configuration */ })
|
||||
.AddMcp(options => { /* Resource metadata configuration */ });
|
||||
builder.Services.AddAuthorization();
|
||||
|
||||
builder.Services.AddMcpServer()
|
||||
.WithHttpTransport()
|
||||
.AddAuthorizationFilters() // Required for authorization support
|
||||
.WithTools<WeatherTools>()
|
||||
.AddCallToolFilter(next => async (context, cancellationToken) =>
|
||||
{
|
||||
// Custom call tool logic
|
||||
return await next(context, cancellationToken);
|
||||
});
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
app.MapMcp();
|
||||
app.Run();
|
||||
```
|
||||
|
||||
### Custom Authorization Filters
|
||||
|
||||
You can also create custom authorization filters using the filter methods:
|
||||
|
||||
```csharp
|
||||
.AddCallToolFilter(next => async (context, cancellationToken) =>
|
||||
{
|
||||
// Custom authorization logic
|
||||
if (context.User?.Identity?.IsAuthenticated != true)
|
||||
{
|
||||
return new CallToolResult
|
||||
{
|
||||
Content = [new TextContent { Text = "Custom: Authentication required" }],
|
||||
IsError = true
|
||||
};
|
||||
}
|
||||
|
||||
return await next(context, cancellationToken);
|
||||
});
|
||||
```
|
||||
|
||||
### RequestContext
|
||||
|
||||
Within filters, you have access to:
|
||||
|
||||
- `context.User` - The current user's `ClaimsPrincipal`
|
||||
- `context.Services` - The request's service provider for resolving authorization services
|
||||
- `context.MatchedPrimitive` - The matched tool/prompt/resource with its metadata including authorization attributes via `context.MatchedPrimitive.Metadata`
|
||||
|
|
@ -13,3 +13,7 @@ items:
|
|||
items:
|
||||
- name: Logging
|
||||
uid: logging
|
||||
- name: Server Features
|
||||
items:
|
||||
- name: Filters
|
||||
uid: filters
|
||||
|
|
@ -7,7 +7,7 @@
|
|||
"applicationUrl": "http://localhost:3001",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"OTEL_SERVICE_NAME": "aspnetcore-mcp-server",
|
||||
"OTEL_SERVICE_NAME": "aspnetcore-mcp-server"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
|
|
@ -16,7 +16,7 @@
|
|||
"applicationUrl": "https://localhost:7133;http://localhost:3001",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"OTEL_SERVICE_NAME": "aspnetcore-mcp-server",
|
||||
"OTEL_SERVICE_NAME": "aspnetcore-mcp-server"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
359
src/ModelContextProtocol.AspNetCore/AuthorizationFilterSetup.cs
Normal file
359
src/ModelContextProtocol.AspNetCore/AuthorizationFilterSetup.cs
Normal file
|
|
@ -0,0 +1,359 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using ModelContextProtocol.Protocol;
|
||||
using ModelContextProtocol.Server;
|
||||
|
||||
namespace ModelContextProtocol.AspNetCore;
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates authorization policies from endpoint metadata.
|
||||
/// </summary>
|
||||
internal sealed class AuthorizationFilterSetup(IAuthorizationPolicyProvider? policyProvider = null) : IConfigureOptions<McpServerOptions>, IPostConfigureOptions<McpServerOptions>
|
||||
{
|
||||
private static readonly string AuthorizationFilterInvokedKey = "ModelContextProtocol.AspNetCore.AuthorizationFilter.Invoked";
|
||||
|
||||
public void Configure(McpServerOptions options)
|
||||
{
|
||||
ConfigureListToolsFilter(options);
|
||||
ConfigureCallToolFilter(options);
|
||||
|
||||
ConfigureListResourcesFilter(options);
|
||||
ConfigureListResourceTemplatesFilter(options);
|
||||
ConfigureReadResourceFilter(options);
|
||||
|
||||
ConfigureListPromptsFilter(options);
|
||||
ConfigureGetPromptFilter(options);
|
||||
}
|
||||
|
||||
public void PostConfigure(string? name, McpServerOptions options)
|
||||
{
|
||||
CheckListToolsFilter(options);
|
||||
CheckCallToolFilter(options);
|
||||
|
||||
CheckListResourcesFilter(options);
|
||||
CheckListResourceTemplatesFilter(options);
|
||||
CheckReadResourceFilter(options);
|
||||
|
||||
CheckListPromptsFilter(options);
|
||||
CheckGetPromptFilter(options);
|
||||
}
|
||||
|
||||
private void ConfigureListToolsFilter(McpServerOptions options)
|
||||
{
|
||||
options.Filters.ListToolsFilters.Add(next => async (context, cancellationToken) =>
|
||||
{
|
||||
context.Items[AuthorizationFilterInvokedKey] = true;
|
||||
|
||||
var result = await next(context, cancellationToken);
|
||||
await FilterAuthorizedItemsAsync(
|
||||
result.Tools, static tool => tool.McpServerTool,
|
||||
context.User, context.Services, context);
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
private void CheckListToolsFilter(McpServerOptions options)
|
||||
{
|
||||
options.Filters.ListToolsFilters.Add(next => async (context, cancellationToken) =>
|
||||
{
|
||||
var result = await next(context, cancellationToken);
|
||||
|
||||
if (HasAuthorizationMetadata(result.Tools.Select(static tool => tool.McpServerTool))
|
||||
&& !context.Items.ContainsKey(AuthorizationFilterInvokedKey))
|
||||
{
|
||||
throw new InvalidOperationException("Authorization filter was not invoked for tools/list operation, but authorization metadata was found on the tools. Ensure that AddAuthorizationFilters() is called on the IMcpServerBuilder to configure authorization filters.");
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
private void ConfigureCallToolFilter(McpServerOptions options)
|
||||
{
|
||||
options.Filters.CallToolFilters.Add(next => async (context, cancellationToken) =>
|
||||
{
|
||||
var authResult = await GetAuthorizationResultAsync(context.User, context.MatchedPrimitive, context.Services, context);
|
||||
if (!authResult.Succeeded)
|
||||
{
|
||||
throw new McpException("Access forbidden: This tool requires authorization.", McpErrorCode.InvalidRequest);
|
||||
}
|
||||
|
||||
context.Items[AuthorizationFilterInvokedKey] = true;
|
||||
|
||||
return await next(context, cancellationToken);
|
||||
});
|
||||
}
|
||||
|
||||
private void CheckCallToolFilter(McpServerOptions options)
|
||||
{
|
||||
options.Filters.CallToolFilters.Add(next => async (context, cancellationToken) =>
|
||||
{
|
||||
if (HasAuthorizationMetadata(context.MatchedPrimitive)
|
||||
&& !context.Items.ContainsKey(AuthorizationFilterInvokedKey))
|
||||
{
|
||||
throw new InvalidOperationException("Authorization filter was not invoked for tools/call operation, but authorization metadata was found on the tool. Ensure that AddAuthorizationFilters() is called on the IMcpServerBuilder to configure authorization filters.");
|
||||
}
|
||||
|
||||
return await next(context, cancellationToken);
|
||||
});
|
||||
}
|
||||
|
||||
private void ConfigureListResourcesFilter(McpServerOptions options)
|
||||
{
|
||||
options.Filters.ListResourcesFilters.Add(next => async (context, cancellationToken) =>
|
||||
{
|
||||
context.Items[AuthorizationFilterInvokedKey] = true;
|
||||
|
||||
var result = await next(context, cancellationToken);
|
||||
await FilterAuthorizedItemsAsync(
|
||||
result.Resources, static resource => resource.McpServerResource,
|
||||
context.User, context.Services, context);
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
private void CheckListResourcesFilter(McpServerOptions options)
|
||||
{
|
||||
options.Filters.ListResourcesFilters.Add(next => async (context, cancellationToken) =>
|
||||
{
|
||||
var result = await next(context, cancellationToken);
|
||||
|
||||
if (HasAuthorizationMetadata(result.Resources.Select(static resource => resource.McpServerResource))
|
||||
&& !context.Items.ContainsKey(AuthorizationFilterInvokedKey))
|
||||
{
|
||||
throw new InvalidOperationException("Authorization filter was not invoked for resources/list operation, but authorization metadata was found on the resources. Ensure that AddAuthorizationFilters() is called on the IMcpServerBuilder to configure authorization filters.");
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
private void ConfigureListResourceTemplatesFilter(McpServerOptions options)
|
||||
{
|
||||
options.Filters.ListResourceTemplatesFilters.Add(next => async (context, cancellationToken) =>
|
||||
{
|
||||
context.Items[AuthorizationFilterInvokedKey] = true;
|
||||
|
||||
var result = await next(context, cancellationToken);
|
||||
await FilterAuthorizedItemsAsync(
|
||||
result.ResourceTemplates, static resourceTemplate => resourceTemplate.McpServerResource,
|
||||
context.User, context.Services, context);
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
private void CheckListResourceTemplatesFilter(McpServerOptions options)
|
||||
{
|
||||
options.Filters.ListResourceTemplatesFilters.Add(next => async (context, cancellationToken) =>
|
||||
{
|
||||
var result = await next(context, cancellationToken);
|
||||
|
||||
if (HasAuthorizationMetadata(result.ResourceTemplates.Select(static resourceTemplate => resourceTemplate.McpServerResource))
|
||||
&& !context.Items.ContainsKey(AuthorizationFilterInvokedKey))
|
||||
{
|
||||
throw new InvalidOperationException("Authorization filter was not invoked for resources/templates/list operation, but authorization metadata was found on the resource templates. Ensure that AddAuthorizationFilters() is called on the IMcpServerBuilder to configure authorization filters.");
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
private void ConfigureReadResourceFilter(McpServerOptions options)
|
||||
{
|
||||
options.Filters.ReadResourceFilters.Add(next => async (context, cancellationToken) =>
|
||||
{
|
||||
context.Items[AuthorizationFilterInvokedKey] = true;
|
||||
|
||||
var authResult = await GetAuthorizationResultAsync(context.User, context.MatchedPrimitive, context.Services, context);
|
||||
if (!authResult.Succeeded)
|
||||
{
|
||||
throw new McpException("Access forbidden: This resource requires authorization.", McpErrorCode.InvalidRequest);
|
||||
}
|
||||
|
||||
return await next(context, cancellationToken);
|
||||
});
|
||||
}
|
||||
|
||||
private void CheckReadResourceFilter(McpServerOptions options)
|
||||
{
|
||||
options.Filters.ReadResourceFilters.Add(next => async (context, cancellationToken) =>
|
||||
{
|
||||
if (HasAuthorizationMetadata(context.MatchedPrimitive)
|
||||
&& !context.Items.ContainsKey(AuthorizationFilterInvokedKey))
|
||||
{
|
||||
throw new InvalidOperationException("Authorization filter was not invoked for resources/read operation, but authorization metadata was found on the resource. Ensure that AddAuthorizationFilters() is called on the IMcpServerBuilder to configure authorization filters.");
|
||||
}
|
||||
|
||||
return await next(context, cancellationToken);
|
||||
});
|
||||
}
|
||||
|
||||
private void ConfigureListPromptsFilter(McpServerOptions options)
|
||||
{
|
||||
options.Filters.ListPromptsFilters.Add(next => async (context, cancellationToken) =>
|
||||
{
|
||||
context.Items[AuthorizationFilterInvokedKey] = true;
|
||||
|
||||
var result = await next(context, cancellationToken);
|
||||
await FilterAuthorizedItemsAsync(
|
||||
result.Prompts, static prompt => prompt.McpServerPrompt,
|
||||
context.User, context.Services, context);
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
private void CheckListPromptsFilter(McpServerOptions options)
|
||||
{
|
||||
options.Filters.ListPromptsFilters.Add(next => async (context, cancellationToken) =>
|
||||
{
|
||||
var result = await next(context, cancellationToken);
|
||||
|
||||
if (HasAuthorizationMetadata(result.Prompts.Select(static prompt => prompt.McpServerPrompt))
|
||||
&& !context.Items.ContainsKey(AuthorizationFilterInvokedKey))
|
||||
{
|
||||
throw new InvalidOperationException("Authorization filter was not invoked for prompts/list operation, but authorization metadata was found on the prompts. Ensure that AddAuthorizationFilters() is called on the IMcpServerBuilder to configure authorization filters.");
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
private void ConfigureGetPromptFilter(McpServerOptions options)
|
||||
{
|
||||
options.Filters.GetPromptFilters.Add(next => async (context, cancellationToken) =>
|
||||
{
|
||||
context.Items[AuthorizationFilterInvokedKey] = true;
|
||||
|
||||
var authResult = await GetAuthorizationResultAsync(context.User, context.MatchedPrimitive, context.Services, context);
|
||||
if (!authResult.Succeeded)
|
||||
{
|
||||
throw new McpException("Access forbidden: This prompt requires authorization.", McpErrorCode.InvalidRequest);
|
||||
}
|
||||
|
||||
return await next(context, cancellationToken);
|
||||
});
|
||||
}
|
||||
|
||||
private void CheckGetPromptFilter(McpServerOptions options)
|
||||
{
|
||||
options.Filters.GetPromptFilters.Add(next => async (context, cancellationToken) =>
|
||||
{
|
||||
if (HasAuthorizationMetadata(context.MatchedPrimitive)
|
||||
&& !context.Items.ContainsKey(AuthorizationFilterInvokedKey))
|
||||
{
|
||||
throw new InvalidOperationException("Authorization filter was not invoked for prompts/get operation, but authorization metadata was found on the prompt. Ensure that AddAuthorizationFilters() is called on the IMcpServerBuilder to configure authorization filters.");
|
||||
}
|
||||
|
||||
return await next(context, cancellationToken);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Filters a collection of items based on authorization policies in their metadata.
|
||||
/// For list operations where we need to filter results by authorization.
|
||||
/// </summary>
|
||||
private async ValueTask FilterAuthorizedItemsAsync<T>(IList<T> items, Func<T, IMcpServerPrimitive?> primitiveSelector,
|
||||
ClaimsPrincipal? user, IServiceProvider? requestServices, object context)
|
||||
{
|
||||
for (int i = items.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var authorizationResult = await GetAuthorizationResultAsync(
|
||||
user, primitiveSelector(items[i]), requestServices, context);
|
||||
|
||||
if (!authorizationResult.Succeeded)
|
||||
{
|
||||
items.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async ValueTask<AuthorizationResult> GetAuthorizationResultAsync(
|
||||
ClaimsPrincipal? user, IMcpServerPrimitive? primitive, IServiceProvider? requestServices, object context)
|
||||
{
|
||||
if (!HasAuthorizationMetadata(primitive))
|
||||
{
|
||||
return AuthorizationResult.Success();
|
||||
}
|
||||
|
||||
if (policyProvider is null)
|
||||
{
|
||||
throw new InvalidOperationException($"You must call AddAuthorization() because an authorization related attribute was found on {primitive.Id}");
|
||||
}
|
||||
|
||||
var policy = await CombineAsync(policyProvider, primitive.Metadata);
|
||||
if (policy is null)
|
||||
{
|
||||
return AuthorizationResult.Success();
|
||||
}
|
||||
|
||||
if (requestServices is null)
|
||||
{
|
||||
// The IAuthorizationPolicyProvider service must be non-null to get to this line, so it's very unexpected for RequestContext.Services to not be set.
|
||||
throw new InvalidOperationException("RequestContext.Services is not set! The IMcpServer must be initialized with a non-null IServiceProvider.");
|
||||
}
|
||||
|
||||
// ASP.NET Core's AuthorizationMiddleware resolves the IAuthorizationService from scoped request services, so we do the same.
|
||||
var authService = requestServices.GetRequiredService<IAuthorizationService>();
|
||||
return await authService.AuthorizeAsync(user ?? new ClaimsPrincipal(new ClaimsIdentity()), context, policy);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Combines authorization policies and requirements from endpoint metadata without considering <see cref="IAllowAnonymous"/>.
|
||||
/// </summary>
|
||||
/// <param name="policyProvider">The authorization policy provider.</param>
|
||||
/// <param name="endpointMetadata">The endpoint metadata collection.</param>
|
||||
/// <returns>The combined authorization policy, or null if no authorization is required.</returns>
|
||||
private static async ValueTask<AuthorizationPolicy?> CombineAsync(IAuthorizationPolicyProvider policyProvider, IReadOnlyList<object> endpointMetadata)
|
||||
{
|
||||
// https://github.com/dotnet/aspnetcore/issues/63365 tracks adding this as public API to AuthorizationPolicy itself.
|
||||
// Copied from https://github.com/dotnet/aspnetcore/blob/9f2977bf9cfb539820983bda3bedf81c8cda9f20/src/Security/Authorization/Policy/src/AuthorizationMiddleware.cs#L116-L138
|
||||
var authorizeData = endpointMetadata.OfType<IAuthorizeData>();
|
||||
var policies = endpointMetadata.OfType<AuthorizationPolicy>();
|
||||
|
||||
var policy = await AuthorizationPolicy.CombineAsync(policyProvider, authorizeData, policies);
|
||||
|
||||
AuthorizationPolicyBuilder? reqPolicyBuilder = null;
|
||||
|
||||
foreach (var m in endpointMetadata)
|
||||
{
|
||||
if (m is not IAuthorizationRequirementData requirementData)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
reqPolicyBuilder ??= new AuthorizationPolicyBuilder();
|
||||
foreach (var requirement in requirementData.GetRequirements())
|
||||
{
|
||||
reqPolicyBuilder.AddRequirements(requirement);
|
||||
}
|
||||
}
|
||||
|
||||
if (reqPolicyBuilder is null)
|
||||
{
|
||||
return policy;
|
||||
}
|
||||
|
||||
// Combine policy with requirements or just use requirements if no policy
|
||||
return (policy is null)
|
||||
? reqPolicyBuilder.Build()
|
||||
: AuthorizationPolicy.Combine(policy, reqPolicyBuilder.Build());
|
||||
}
|
||||
|
||||
private static bool HasAuthorizationMetadata([NotNullWhen(true)] IMcpServerPrimitive? primitive)
|
||||
{
|
||||
// If no primitive was found for this request or there is IAllowAnonymous metadata anywhere on the class or method,
|
||||
// the request should go through as normal.
|
||||
if (primitive is null || primitive.Metadata.Any(static m => m is IAllowAnonymous))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return primitive.Metadata.Any(static m => m is IAuthorizeData or AuthorizationPolicy or IAuthorizationRequirementData);
|
||||
}
|
||||
|
||||
private static bool HasAuthorizationMetadata(IEnumerable<IMcpServerPrimitive?> primitives)
|
||||
=> primitives.Any(HasAuthorizationMetadata);
|
||||
}
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using ModelContextProtocol.AspNetCore;
|
||||
using ModelContextProtocol.Server;
|
||||
|
||||
|
|
@ -29,6 +31,8 @@ public static class HttpMcpServerBuilderExtensions
|
|||
builder.Services.AddHostedService<IdleTrackingBackgroundService>();
|
||||
builder.Services.AddDataProtection();
|
||||
|
||||
builder.Services.TryAddEnumerable(ServiceDescriptor.Transient<IPostConfigureOptions<McpServerOptions>, AuthorizationFilterSetup>());
|
||||
|
||||
if (configureOptions is not null)
|
||||
{
|
||||
builder.Services.Configure(configureOptions);
|
||||
|
|
@ -36,4 +40,27 @@ public static class HttpMcpServerBuilderExtensions
|
|||
|
||||
return builder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds authorization filters to support <see cref="AuthorizeAttribute"/>
|
||||
/// on MCP server tools, prompts, and resources. This method should always be called when using
|
||||
/// ASP.NET Core integration to ensure proper authorization support.
|
||||
/// </summary>
|
||||
/// <param name="builder">The builder instance.</param>
|
||||
/// <returns>The builder provided in <paramref name="builder"/>.</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception>
|
||||
/// <remarks>
|
||||
/// This method automatically configures authorization filters for all MCP server handlers. These filters respect
|
||||
/// authorization attributes such as <see cref="AuthorizeAttribute"/>
|
||||
/// and <see cref="AllowAnonymousAttribute"/>.
|
||||
/// </remarks>
|
||||
public static IMcpServerBuilder AddAuthorizationFilters(this IMcpServerBuilder builder)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(builder);
|
||||
|
||||
// Allow the authorization filters to get added multiple times in case other middleware changes the matched primitive.
|
||||
builder.Services.AddTransient<IConfigureOptions<McpServerOptions>, AuthorizationFilterSetup>();
|
||||
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using ModelContextProtocol.Protocol;
|
||||
using ModelContextProtocol.Server;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
|
|
@ -97,7 +96,7 @@ internal sealed class SseHandler(
|
|||
return;
|
||||
}
|
||||
|
||||
var message = (JsonRpcMessage?)await context.Request.ReadFromJsonAsync(McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(JsonRpcMessage)), context.RequestAborted);
|
||||
var message = await StreamableHttpHandler.ReadJsonRpcMessageAsync(context);
|
||||
if (message is null)
|
||||
{
|
||||
await Results.BadRequest("No message in request body.").ExecuteAsync(context);
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ using Microsoft.Net.Http.Headers;
|
|||
using ModelContextProtocol.AspNetCore.Stateless;
|
||||
using ModelContextProtocol.Protocol;
|
||||
using ModelContextProtocol.Server;
|
||||
using System.IO.Pipelines;
|
||||
using System.Security.Claims;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.Json;
|
||||
|
|
@ -26,6 +25,8 @@ internal sealed class StreamableHttpHandler(
|
|||
IServiceProvider applicationServices)
|
||||
{
|
||||
private const string McpSessionIdHeaderName = "Mcp-Session-Id";
|
||||
|
||||
private static readonly JsonTypeInfo<JsonRpcMessage> s_messageTypeInfo = GetRequiredJsonTypeInfo<JsonRpcMessage>();
|
||||
private static readonly JsonTypeInfo<JsonRpcError> s_errorTypeInfo = GetRequiredJsonTypeInfo<JsonRpcError>();
|
||||
|
||||
public HttpServerTransportOptions HttpServerTransportOptions => httpServerTransportOptions.Value;
|
||||
|
|
@ -55,8 +56,17 @@ internal sealed class StreamableHttpHandler(
|
|||
|
||||
await using var _ = await session.AcquireReferenceAsync(context.RequestAborted);
|
||||
|
||||
var message = await ReadJsonRpcMessageAsync(context);
|
||||
if (message is null)
|
||||
{
|
||||
await WriteJsonRpcErrorAsync(context,
|
||||
"Bad Request: The POST body did not contain a valid JSON-RPC message.",
|
||||
StatusCodes.Status400BadRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
InitializeSseResponse(context);
|
||||
var wroteResponse = await session.Transport.HandlePostRequest(new HttpDuplexPipe(context), context.RequestAborted);
|
||||
var wroteResponse = await session.Transport.HandlePostRequest(message, context.Response.Body, context.RequestAborted);
|
||||
if (!wroteResponse)
|
||||
{
|
||||
// We wound up writing nothing, so there should be no Content-Type response header.
|
||||
|
|
@ -264,6 +274,22 @@ internal sealed class StreamableHttpHandler(
|
|||
return WebEncoders.Base64UrlEncode(buffer);
|
||||
}
|
||||
|
||||
internal static async Task<JsonRpcMessage?> ReadJsonRpcMessageAsync(HttpContext context)
|
||||
{
|
||||
// Implementation for reading a JSON-RPC message from the request body
|
||||
var message = await context.Request.ReadFromJsonAsync(s_messageTypeInfo, context.RequestAborted);
|
||||
|
||||
if (context.User?.Identity?.IsAuthenticated == true && message is not null)
|
||||
{
|
||||
message.Context = new()
|
||||
{
|
||||
User = context.User,
|
||||
};
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
private void ScheduleStatelessSessionIdWrite(HttpContext context, StreamableHttpServerTransport transport)
|
||||
{
|
||||
transport.OnInitRequestReceived = initRequestParams =>
|
||||
|
|
@ -304,17 +330,11 @@ internal sealed class StreamableHttpHandler(
|
|||
return null;
|
||||
}
|
||||
|
||||
private static JsonTypeInfo<T> GetRequiredJsonTypeInfo<T>() => (JsonTypeInfo<T>)McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(T));
|
||||
internal static JsonTypeInfo<T> GetRequiredJsonTypeInfo<T>() => (JsonTypeInfo<T>)McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(T));
|
||||
|
||||
private static bool MatchesApplicationJsonMediaType(MediaTypeHeaderValue acceptHeaderValue)
|
||||
=> acceptHeaderValue.MatchesMediaType("application/json");
|
||||
|
||||
private static bool MatchesTextEventStreamMediaType(MediaTypeHeaderValue acceptHeaderValue)
|
||||
=> acceptHeaderValue.MatchesMediaType("text/event-stream");
|
||||
|
||||
private sealed class HttpDuplexPipe(HttpContext context) : IDuplexPipe
|
||||
{
|
||||
public PipeReader Input => context.Request.BodyReader;
|
||||
public PipeWriter Output => context.Response.BodyWriter;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -100,15 +100,17 @@ internal sealed class StreamableHttpSession(
|
|||
|
||||
try
|
||||
{
|
||||
await _disposeCts.CancelAsync();
|
||||
|
||||
try
|
||||
{
|
||||
// Dispose transport first to complete the incoming MessageReader gracefully and avoid a potentially unnecessary OCE.
|
||||
await transport.DisposeAsync();
|
||||
await _disposeCts.CancelAsync();
|
||||
|
||||
await ServerRunTask;
|
||||
}
|
||||
finally
|
||||
{
|
||||
await DisposeServerThenTransportAsync();
|
||||
await server.DisposeAsync();
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
|
|
@ -124,18 +126,6 @@ internal sealed class StreamableHttpSession(
|
|||
}
|
||||
}
|
||||
|
||||
private async ValueTask DisposeServerThenTransportAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await server.DisposeAsync();
|
||||
}
|
||||
finally
|
||||
{
|
||||
await transport.DisposeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class UnreferenceDisposable(StreamableHttpSession session) : IAsyncDisposable
|
||||
{
|
||||
public ValueTask DisposeAsync()
|
||||
|
|
|
|||
|
|
@ -116,14 +116,14 @@ internal sealed partial class McpSession : IDisposable
|
|||
LogMessageRead(EndpointName, message.GetType().Name);
|
||||
|
||||
// Fire and forget the message handling to avoid blocking the transport.
|
||||
if (message.ExecutionContext is null)
|
||||
if (message.Context?.ExecutionContext is null)
|
||||
{
|
||||
_ = ProcessMessageAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Flow the execution context from the HTTP request corresponding to this message if provided.
|
||||
ExecutionContext.Run(message.ExecutionContext, _ => _ = ProcessMessageAsync(), null);
|
||||
ExecutionContext.Run(message.Context.ExecutionContext, _ => _ = ProcessMessageAsync(), null);
|
||||
}
|
||||
|
||||
async Task ProcessMessageAsync()
|
||||
|
|
@ -176,13 +176,15 @@ internal sealed partial class McpSession : IDisposable
|
|||
Message = "An error occurred.",
|
||||
};
|
||||
|
||||
await SendMessageAsync(new JsonRpcError
|
||||
var errorMessage = new JsonRpcError
|
||||
{
|
||||
Id = request.Id,
|
||||
JsonRpc = "2.0",
|
||||
Error = detail,
|
||||
RelatedTransport = request.RelatedTransport,
|
||||
}, cancellationToken).ConfigureAwait(false);
|
||||
Context = new JsonRpcMessageContext { RelatedTransport = request.Context?.RelatedTransport },
|
||||
};
|
||||
|
||||
await SendMessageAsync(errorMessage, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else if (ex is not OperationCanceledException)
|
||||
{
|
||||
|
|
@ -329,7 +331,7 @@ internal sealed partial class McpSession : IDisposable
|
|||
{
|
||||
Id = request.Id,
|
||||
Result = result,
|
||||
RelatedTransport = request.RelatedTransport,
|
||||
Context = request.Context,
|
||||
}, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return result;
|
||||
|
|
@ -349,7 +351,7 @@ internal sealed partial class McpSession : IDisposable
|
|||
{
|
||||
Method = NotificationMethods.CancelledNotification,
|
||||
Params = JsonSerializer.SerializeToNode(new CancelledNotificationParams { RequestId = state.Item2.Id }, McpJsonUtilities.JsonContext.Default.CancelledNotificationParams),
|
||||
RelatedTransport = state.Item2.RelatedTransport,
|
||||
Context = new JsonRpcMessageContext { RelatedTransport = state.Item2.Context?.RelatedTransport },
|
||||
});
|
||||
}, Tuple.Create(this, request));
|
||||
}
|
||||
|
|
@ -527,7 +529,7 @@ internal sealed partial class McpSession : IDisposable
|
|||
// Streamable HTTP transport where the specification states that the server SHOULD include JSON-RPC responses in
|
||||
// the HTTP response body for the POST request containing the corresponding JSON-RPC request.
|
||||
private Task SendToRelatedTransportAsync(JsonRpcMessage message, CancellationToken cancellationToken)
|
||||
=> (message.RelatedTransport ?? _transport).SendMessageAsync(message, cancellationToken);
|
||||
=> (message.Context?.RelatedTransport ?? _transport).SendMessageAsync(message, cancellationToken);
|
||||
|
||||
private static CancelledNotificationParams? GetCancelledNotificationParams(JsonNode? notificationParams)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ namespace ModelContextProtocol.Protocol;
|
|||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// When enabled, this capability allows a Model Context Protocol server to provide
|
||||
/// When enabled, this capability allows a Model Context Protocol server to provide
|
||||
/// auto-completion suggestions. This capability is advertised to clients during the initialize handshake.
|
||||
/// </para>
|
||||
/// <para>
|
||||
|
|
@ -33,5 +33,5 @@ public sealed class CompletionsCapability
|
|||
/// and should return appropriate completion suggestions.
|
||||
/// </remarks>
|
||||
[JsonIgnore]
|
||||
public Func<RequestContext<CompleteRequestParams>, CancellationToken, ValueTask<CompleteResult>>? CompleteHandler { get; set; }
|
||||
public McpRequestHandler<CompleteRequestParams, CompleteResult>? CompleteHandler { get; set; }
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
using ModelContextProtocol.Server;
|
||||
using System.ComponentModel;
|
||||
using System.Security.Claims;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
|
|
@ -29,28 +30,21 @@ public abstract class JsonRpcMessage
|
|||
public string JsonRpc { get; init; } = "2.0";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the transport the <see cref="JsonRpcMessage"/> was received on or should be sent over.
|
||||
/// Gets or sets the contextual information for this JSON-RPC message.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is used to support the Streamable HTTP transport where the specification states that the server
|
||||
/// SHOULD include JSON-RPC responses in the HTTP response body for the POST request containing
|
||||
/// the corresponding JSON-RPC request. It may be <see langword="null"/> for other transports.
|
||||
/// This property contains transport-specific and runtime context information that accompanies
|
||||
/// JSON-RPC messages but is not serialized as part of the JSON-RPC payload. This includes
|
||||
/// transport references, execution context, and authenticated user information.
|
||||
/// </remarks>
|
||||
[JsonIgnore]
|
||||
public ITransport? RelatedTransport { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="ExecutionContext"/> that should be used to run any handlers
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is used to support the Streamable HTTP transport in its default stateful mode. In this mode,
|
||||
/// the <see cref="IMcpServer"/> outlives the initial HTTP request context it was created on, and new
|
||||
/// JSON-RPC messages can originate from future HTTP requests. This allows the transport to flow the
|
||||
/// context with the JSON-RPC message. This is particularly useful for enabling IHttpContextAccessor
|
||||
/// in tool calls.
|
||||
/// This property should only be set when implementing a custom <see cref="ITransport"/>
|
||||
/// that needs to pass additional per-message context or to pass a <see cref="JsonRpcMessageContext.User"/>
|
||||
/// to <see cref="StreamableHttpServerTransport.HandlePostRequest(JsonRpcMessage, Stream, CancellationToken)"/>
|
||||
/// or <see cref="SseResponseStreamTransport.OnMessageReceivedAsync(JsonRpcMessage, CancellationToken)"/> .
|
||||
/// </remarks>
|
||||
[JsonIgnore]
|
||||
public ExecutionContext? ExecutionContext { get; set; }
|
||||
public JsonRpcMessageContext? Context { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Provides a <see cref="JsonConverter"/> for <see cref="JsonRpcMessage"/> messages,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,61 @@
|
|||
using ModelContextProtocol.Server;
|
||||
using System.Security.Claims;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace ModelContextProtocol.Protocol;
|
||||
|
||||
/// <summary>
|
||||
/// Contains contextual information for JSON-RPC messages that is not part of the JSON-RPC protocol specification.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This class holds transport-specific and runtime context information that accompanies JSON-RPC messages
|
||||
/// but is not serialized as part of the JSON-RPC payload. This includes transport references, execution context,
|
||||
/// and authenticated user information.
|
||||
/// </remarks>
|
||||
public class JsonRpcMessageContext
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the transport the <see cref="JsonRpcMessage"/> was received on or should be sent over.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is used to support the Streamable HTTP transport where the specification states that the server
|
||||
/// SHOULD include JSON-RPC responses in the HTTP response body for the POST request containing
|
||||
/// the corresponding JSON-RPC request. It may be <see langword="null"/> for other transports.
|
||||
/// </remarks>
|
||||
public ITransport? RelatedTransport { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="ExecutionContext"/> that should be used to run any handlers
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is used to support the Streamable HTTP transport in its default stateful mode. In this mode,
|
||||
/// the <see cref="IMcpServer"/> outlives the initial HTTP request context it was created on, and new
|
||||
/// JSON-RPC messages can originate from future HTTP requests. This allows the transport to flow the
|
||||
/// context with the JSON-RPC message. This is particularly useful for enabling IHttpContextAccessor
|
||||
/// in tool calls.
|
||||
/// </remarks>
|
||||
public ExecutionContext? ExecutionContext { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the authenticated user associated with this JSON-RPC message.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This property contains the <see cref="ClaimsPrincipal"/> representing the authenticated user
|
||||
/// who initiated this JSON-RPC message. This enables request handlers to access user identity
|
||||
/// and authorization information without requiring dependency on HTTP context accessors
|
||||
/// or other HTTP-specific abstractions.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The user information is automatically populated by the transport layer when processing
|
||||
/// incoming HTTP requests in ASP.NET Core scenarios. For other transport types or scenarios
|
||||
/// where user authentication is not applicable, this property may be <see langword="null"/>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This property is particularly useful in the Streamable HTTP transport where JSON-RPC messages
|
||||
/// may outlive the original HTTP request context, allowing user identity to be preserved
|
||||
/// throughout the message processing pipeline.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public ClaimsPrincipal? User { get; set; }
|
||||
}
|
||||
|
|
@ -9,7 +9,7 @@ namespace ModelContextProtocol.Protocol;
|
|||
/// <remarks>
|
||||
/// Requests are messages that require a response from the receiver. Each request includes a unique ID
|
||||
/// that will be included in the corresponding response message (either a success response or an error).
|
||||
///
|
||||
///
|
||||
/// The receiver of a request message is expected to execute the specified method with the provided parameters
|
||||
/// and return either a <see cref="JsonRpcResponse"/> with the result, or a <see cref="JsonRpcError"/>
|
||||
/// if the method execution fails.
|
||||
|
|
@ -36,7 +36,7 @@ public sealed class JsonRpcRequest : JsonRpcMessageWithId
|
|||
Id = id,
|
||||
Method = Method,
|
||||
Params = Params,
|
||||
RelatedTransport = RelatedTransport,
|
||||
Context = Context,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,5 +18,5 @@ public sealed class LoggingCapability
|
|||
/// Gets or sets the handler for set logging level requests from clients.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public Func<RequestContext<SetLevelRequestParams>, CancellationToken, ValueTask<EmptyResult>>? SetLoggingLevelHandler { get; set; }
|
||||
public McpRequestHandler<SetLevelRequestParams, EmptyResult>? SetLoggingLevelHandler { get; set; }
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
using System.Text.Json.Nodes;
|
||||
using System.Text.Json.Serialization;
|
||||
using ModelContextProtocol.Server;
|
||||
|
||||
namespace ModelContextProtocol.Protocol;
|
||||
|
||||
|
|
@ -59,4 +60,10 @@ public sealed class Prompt : IBaseMetadata
|
|||
/// </remarks>
|
||||
[JsonPropertyName("_meta")]
|
||||
public JsonObject? Meta { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the callable server prompt corresponding to this metadata if any.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public McpServerPrompt? McpServerPrompt { get; set; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,10 +22,10 @@ public sealed class PromptsCapability
|
|||
/// Gets or sets whether this server supports notifications for changes to the prompt list.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When set to <see langword="true"/>, the server will send notifications using
|
||||
/// <see cref="NotificationMethods.PromptListChangedNotification"/> when prompts are added,
|
||||
/// When set to <see langword="true"/>, the server will send notifications using
|
||||
/// <see cref="NotificationMethods.PromptListChangedNotification"/> when prompts are added,
|
||||
/// removed, or modified. Clients can register handlers for these notifications to
|
||||
/// refresh their prompt cache. This capability enables clients to stay synchronized with server-side changes
|
||||
/// refresh their prompt cache. This capability enables clients to stay synchronized with server-side changes
|
||||
/// to available prompts.
|
||||
/// </remarks>
|
||||
[JsonPropertyName("listChanged")]
|
||||
|
|
@ -40,15 +40,15 @@ public sealed class PromptsCapability
|
|||
/// along with any prompts defined in <see cref="PromptCollection"/>.
|
||||
/// </remarks>
|
||||
[JsonIgnore]
|
||||
public Func<RequestContext<ListPromptsRequestParams>, CancellationToken, ValueTask<ListPromptsResult>>? ListPromptsHandler { get; set; }
|
||||
public McpRequestHandler<ListPromptsRequestParams, ListPromptsResult>? ListPromptsHandler { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the handler for <see cref="RequestMethods.PromptsGet"/> requests.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This handler is invoked when a client requests details for a specific prompt by name and provides arguments
|
||||
/// for the prompt if needed. The handler receives the request context containing the prompt name and any arguments,
|
||||
/// This handler is invoked when a client requests details for a specific prompt by name and provides arguments
|
||||
/// for the prompt if needed. The handler receives the request context containing the prompt name and any arguments,
|
||||
/// and should return a <see cref="GetPromptResult"/> with the prompt messages and other details.
|
||||
/// </para>
|
||||
/// <para>
|
||||
|
|
@ -57,7 +57,7 @@ public sealed class PromptsCapability
|
|||
/// </para>
|
||||
/// </remarks>
|
||||
[JsonIgnore]
|
||||
public Func<RequestContext<GetPromptRequestParams>, CancellationToken, ValueTask<GetPromptResult>>? GetPromptHandler { get; set; }
|
||||
public McpRequestHandler<GetPromptRequestParams, GetPromptResult>? GetPromptHandler { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a collection of prompts that will be served by the server.
|
||||
|
|
@ -69,7 +69,7 @@ public sealed class PromptsCapability
|
|||
/// when those are provided:
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// - For <see cref="RequestMethods.PromptsList"/> requests: The server returns all prompts from this collection
|
||||
/// - For <see cref="RequestMethods.PromptsList"/> requests: The server returns all prompts from this collection
|
||||
/// plus any additional prompts provided by the <see cref="ListPromptsHandler"/> if it's set.
|
||||
/// </para>
|
||||
/// <para>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System.Text.Json.Nodes;
|
||||
using System.Text.Json.Serialization;
|
||||
using ModelContextProtocol.Server;
|
||||
|
||||
namespace ModelContextProtocol.Protocol;
|
||||
|
||||
|
|
@ -87,4 +88,10 @@ public sealed class Resource : IBaseMetadata
|
|||
/// </remarks>
|
||||
[JsonPropertyName("_meta")]
|
||||
public JsonObject? Meta { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the callable server resource corresponding to this metadata if any.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public McpServerResource? McpServerResource { get; set; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System.Text.Json.Nodes;
|
||||
using System.Text.Json.Serialization;
|
||||
using ModelContextProtocol.Server;
|
||||
|
||||
namespace ModelContextProtocol.Protocol;
|
||||
|
||||
|
|
@ -84,6 +85,12 @@ public sealed class ResourceTemplate : IBaseMetadata
|
|||
[JsonIgnore]
|
||||
public bool IsTemplated => UriTemplate.Contains('{');
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the callable server resource corresponding to this metadata if any.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public McpServerResource? McpServerResource { get; set; }
|
||||
|
||||
/// <summary>Converts the <see cref="ResourceTemplate"/> into a <see cref="Resource"/>.</summary>
|
||||
/// <returns>A <see cref="Resource"/> if <see cref="IsTemplated"/> is <see langword="false"/>; otherwise, <see langword="null"/>.</returns>
|
||||
public Resource? AsResource()
|
||||
|
|
@ -102,6 +109,7 @@ public sealed class ResourceTemplate : IBaseMetadata
|
|||
MimeType = MimeType,
|
||||
Annotations = Annotations,
|
||||
Meta = Meta,
|
||||
McpServerResource = McpServerResource,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -21,8 +21,8 @@ public sealed class ResourcesCapability
|
|||
/// Gets or sets whether this server supports notifications for changes to the resource list.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When set to <see langword="true"/>, the server will send notifications using
|
||||
/// <see cref="NotificationMethods.ResourceListChangedNotification"/> when resources are added,
|
||||
/// When set to <see langword="true"/>, the server will send notifications using
|
||||
/// <see cref="NotificationMethods.ResourceListChangedNotification"/> when resources are added,
|
||||
/// removed, or modified. Clients can register handlers for these notifications to
|
||||
/// refresh their resource cache.
|
||||
/// </remarks>
|
||||
|
|
@ -39,7 +39,7 @@ public sealed class ResourcesCapability
|
|||
/// allowing clients to discover available resource types and their access patterns.
|
||||
/// </remarks>
|
||||
[JsonIgnore]
|
||||
public Func<RequestContext<ListResourceTemplatesRequestParams>, CancellationToken, ValueTask<ListResourceTemplatesResult>>? ListResourceTemplatesHandler { get; set; }
|
||||
public McpRequestHandler<ListResourceTemplatesRequestParams, ListResourceTemplatesResult>? ListResourceTemplatesHandler { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the handler for <see cref="RequestMethods.ResourcesList"/> requests.
|
||||
|
|
@ -49,7 +49,7 @@ public sealed class ResourcesCapability
|
|||
/// The implementation should return a <see cref="ListResourcesResult"/> with the matching resources.
|
||||
/// </remarks>
|
||||
[JsonIgnore]
|
||||
public Func<RequestContext<ListResourcesRequestParams>, CancellationToken, ValueTask<ListResourcesResult>>? ListResourcesHandler { get; set; }
|
||||
public McpRequestHandler<ListResourcesRequestParams, ListResourcesResult>? ListResourcesHandler { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the handler for <see cref="RequestMethods.ResourcesRead"/> requests.
|
||||
|
|
@ -61,7 +61,7 @@ public sealed class ResourcesCapability
|
|||
/// its contents in a ReadResourceResult object.
|
||||
/// </remarks>
|
||||
[JsonIgnore]
|
||||
public Func<RequestContext<ReadResourceRequestParams>, CancellationToken, ValueTask<ReadResourceResult>>? ReadResourceHandler { get; set; }
|
||||
public McpRequestHandler<ReadResourceRequestParams, ReadResourceResult>? ReadResourceHandler { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the handler for <see cref="RequestMethods.ResourcesSubscribe"/> requests.
|
||||
|
|
@ -74,7 +74,7 @@ public sealed class ResourcesCapability
|
|||
/// requiring polling.
|
||||
/// </remarks>
|
||||
[JsonIgnore]
|
||||
public Func<RequestContext<SubscribeRequestParams>, CancellationToken, ValueTask<EmptyResult>>? SubscribeToResourcesHandler { get; set; }
|
||||
public McpRequestHandler<SubscribeRequestParams, EmptyResult>? SubscribeToResourcesHandler { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the handler for <see cref="RequestMethods.ResourcesUnsubscribe"/> requests.
|
||||
|
|
@ -85,7 +85,7 @@ public sealed class ResourcesCapability
|
|||
/// about the specified resource.
|
||||
/// </remarks>
|
||||
[JsonIgnore]
|
||||
public Func<RequestContext<UnsubscribeRequestParams>, CancellationToken, ValueTask<EmptyResult>>? UnsubscribeFromResourcesHandler { get; set; }
|
||||
public McpRequestHandler<UnsubscribeRequestParams, EmptyResult>? UnsubscribeFromResourcesHandler { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a collection of resources served by the server.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.Json.Serialization;
|
||||
using ModelContextProtocol.Server;
|
||||
|
||||
namespace ModelContextProtocol.Protocol;
|
||||
|
||||
|
|
@ -43,7 +44,7 @@ public sealed class Tool : IBaseMetadata
|
|||
/// if an invalid schema is provided.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The schema typically defines the properties (parameters) that the tool accepts,
|
||||
/// The schema typically defines the properties (parameters) that the tool accepts,
|
||||
/// their types, and which ones are required. This helps AI models understand
|
||||
/// how to structure their calls to the tool.
|
||||
/// </para>
|
||||
|
|
@ -52,9 +53,9 @@ public sealed class Tool : IBaseMetadata
|
|||
/// </para>
|
||||
/// </remarks>
|
||||
[JsonPropertyName("inputSchema")]
|
||||
public JsonElement InputSchema
|
||||
{
|
||||
get => field;
|
||||
public JsonElement InputSchema
|
||||
{
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
if (!McpJsonUtilities.IsValidMcpToolSchema(value))
|
||||
|
|
@ -114,4 +115,10 @@ public sealed class Tool : IBaseMetadata
|
|||
/// </remarks>
|
||||
[JsonPropertyName("_meta")]
|
||||
public JsonObject? Meta { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the callable server tool corresponding to this metadata if any.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public McpServerTool? McpServerTool { get; set; }
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue