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:
|
items:
|
||||||
- name: Logging
|
- name: Logging
|
||||||
uid: logging
|
uid: logging
|
||||||
|
- name: Server Features
|
||||||
|
items:
|
||||||
|
- name: Filters
|
||||||
|
uid: filters
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
"applicationUrl": "http://localhost:3001",
|
"applicationUrl": "http://localhost:3001",
|
||||||
"environmentVariables": {
|
"environmentVariables": {
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||||
"OTEL_SERVICE_NAME": "aspnetcore-mcp-server",
|
"OTEL_SERVICE_NAME": "aspnetcore-mcp-server"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"https": {
|
"https": {
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
"applicationUrl": "https://localhost:7133;http://localhost:3001",
|
"applicationUrl": "https://localhost:7133;http://localhost:3001",
|
||||||
"environmentVariables": {
|
"environmentVariables": {
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
"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.DependencyInjection.Extensions;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
using ModelContextProtocol.AspNetCore;
|
using ModelContextProtocol.AspNetCore;
|
||||||
using ModelContextProtocol.Server;
|
using ModelContextProtocol.Server;
|
||||||
|
|
||||||
|
|
@ -29,6 +31,8 @@ public static class HttpMcpServerBuilderExtensions
|
||||||
builder.Services.AddHostedService<IdleTrackingBackgroundService>();
|
builder.Services.AddHostedService<IdleTrackingBackgroundService>();
|
||||||
builder.Services.AddDataProtection();
|
builder.Services.AddDataProtection();
|
||||||
|
|
||||||
|
builder.Services.TryAddEnumerable(ServiceDescriptor.Transient<IPostConfigureOptions<McpServerOptions>, AuthorizationFilterSetup>());
|
||||||
|
|
||||||
if (configureOptions is not null)
|
if (configureOptions is not null)
|
||||||
{
|
{
|
||||||
builder.Services.Configure(configureOptions);
|
builder.Services.Configure(configureOptions);
|
||||||
|
|
@ -36,4 +40,27 @@ public static class HttpMcpServerBuilderExtensions
|
||||||
|
|
||||||
return builder;
|
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.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using ModelContextProtocol.Protocol;
|
|
||||||
using ModelContextProtocol.Server;
|
using ModelContextProtocol.Server;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
|
@ -97,7 +96,7 @@ internal sealed class SseHandler(
|
||||||
return;
|
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)
|
if (message is null)
|
||||||
{
|
{
|
||||||
await Results.BadRequest("No message in request body.").ExecuteAsync(context);
|
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.AspNetCore.Stateless;
|
||||||
using ModelContextProtocol.Protocol;
|
using ModelContextProtocol.Protocol;
|
||||||
using ModelContextProtocol.Server;
|
using ModelContextProtocol.Server;
|
||||||
using System.IO.Pipelines;
|
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
@ -26,6 +25,8 @@ internal sealed class StreamableHttpHandler(
|
||||||
IServiceProvider applicationServices)
|
IServiceProvider applicationServices)
|
||||||
{
|
{
|
||||||
private const string McpSessionIdHeaderName = "Mcp-Session-Id";
|
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>();
|
private static readonly JsonTypeInfo<JsonRpcError> s_errorTypeInfo = GetRequiredJsonTypeInfo<JsonRpcError>();
|
||||||
|
|
||||||
public HttpServerTransportOptions HttpServerTransportOptions => httpServerTransportOptions.Value;
|
public HttpServerTransportOptions HttpServerTransportOptions => httpServerTransportOptions.Value;
|
||||||
|
|
@ -55,8 +56,17 @@ internal sealed class StreamableHttpHandler(
|
||||||
|
|
||||||
await using var _ = await session.AcquireReferenceAsync(context.RequestAborted);
|
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);
|
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)
|
if (!wroteResponse)
|
||||||
{
|
{
|
||||||
// We wound up writing nothing, so there should be no Content-Type response header.
|
// 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);
|
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)
|
private void ScheduleStatelessSessionIdWrite(HttpContext context, StreamableHttpServerTransport transport)
|
||||||
{
|
{
|
||||||
transport.OnInitRequestReceived = initRequestParams =>
|
transport.OnInitRequestReceived = initRequestParams =>
|
||||||
|
|
@ -304,17 +330,11 @@ internal sealed class StreamableHttpHandler(
|
||||||
return null;
|
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)
|
private static bool MatchesApplicationJsonMediaType(MediaTypeHeaderValue acceptHeaderValue)
|
||||||
=> acceptHeaderValue.MatchesMediaType("application/json");
|
=> acceptHeaderValue.MatchesMediaType("application/json");
|
||||||
|
|
||||||
private static bool MatchesTextEventStreamMediaType(MediaTypeHeaderValue acceptHeaderValue)
|
private static bool MatchesTextEventStreamMediaType(MediaTypeHeaderValue acceptHeaderValue)
|
||||||
=> acceptHeaderValue.MatchesMediaType("text/event-stream");
|
=> 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
|
try
|
||||||
{
|
{
|
||||||
await _disposeCts.CancelAsync();
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
// Dispose transport first to complete the incoming MessageReader gracefully and avoid a potentially unnecessary OCE.
|
||||||
|
await transport.DisposeAsync();
|
||||||
|
await _disposeCts.CancelAsync();
|
||||||
|
|
||||||
await ServerRunTask;
|
await ServerRunTask;
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
await DisposeServerThenTransportAsync();
|
await server.DisposeAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
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
|
private sealed class UnreferenceDisposable(StreamableHttpSession session) : IAsyncDisposable
|
||||||
{
|
{
|
||||||
public ValueTask DisposeAsync()
|
public ValueTask DisposeAsync()
|
||||||
|
|
|
||||||
|
|
@ -116,14 +116,14 @@ internal sealed partial class McpSession : IDisposable
|
||||||
LogMessageRead(EndpointName, message.GetType().Name);
|
LogMessageRead(EndpointName, message.GetType().Name);
|
||||||
|
|
||||||
// Fire and forget the message handling to avoid blocking the transport.
|
// Fire and forget the message handling to avoid blocking the transport.
|
||||||
if (message.ExecutionContext is null)
|
if (message.Context?.ExecutionContext is null)
|
||||||
{
|
{
|
||||||
_ = ProcessMessageAsync();
|
_ = ProcessMessageAsync();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Flow the execution context from the HTTP request corresponding to this message if provided.
|
// 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()
|
async Task ProcessMessageAsync()
|
||||||
|
|
@ -176,13 +176,15 @@ internal sealed partial class McpSession : IDisposable
|
||||||
Message = "An error occurred.",
|
Message = "An error occurred.",
|
||||||
};
|
};
|
||||||
|
|
||||||
await SendMessageAsync(new JsonRpcError
|
var errorMessage = new JsonRpcError
|
||||||
{
|
{
|
||||||
Id = request.Id,
|
Id = request.Id,
|
||||||
JsonRpc = "2.0",
|
JsonRpc = "2.0",
|
||||||
Error = detail,
|
Error = detail,
|
||||||
RelatedTransport = request.RelatedTransport,
|
Context = new JsonRpcMessageContext { RelatedTransport = request.Context?.RelatedTransport },
|
||||||
}, cancellationToken).ConfigureAwait(false);
|
};
|
||||||
|
|
||||||
|
await SendMessageAsync(errorMessage, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
else if (ex is not OperationCanceledException)
|
else if (ex is not OperationCanceledException)
|
||||||
{
|
{
|
||||||
|
|
@ -329,7 +331,7 @@ internal sealed partial class McpSession : IDisposable
|
||||||
{
|
{
|
||||||
Id = request.Id,
|
Id = request.Id,
|
||||||
Result = result,
|
Result = result,
|
||||||
RelatedTransport = request.RelatedTransport,
|
Context = request.Context,
|
||||||
}, cancellationToken).ConfigureAwait(false);
|
}, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|
@ -349,7 +351,7 @@ internal sealed partial class McpSession : IDisposable
|
||||||
{
|
{
|
||||||
Method = NotificationMethods.CancelledNotification,
|
Method = NotificationMethods.CancelledNotification,
|
||||||
Params = JsonSerializer.SerializeToNode(new CancelledNotificationParams { RequestId = state.Item2.Id }, McpJsonUtilities.JsonContext.Default.CancelledNotificationParams),
|
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));
|
}, 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
|
// 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.
|
// the HTTP response body for the POST request containing the corresponding JSON-RPC request.
|
||||||
private Task SendToRelatedTransportAsync(JsonRpcMessage message, CancellationToken cancellationToken)
|
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)
|
private static CancelledNotificationParams? GetCancelledNotificationParams(JsonNode? notificationParams)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ namespace ModelContextProtocol.Protocol;
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// <para>
|
/// <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.
|
/// auto-completion suggestions. This capability is advertised to clients during the initialize handshake.
|
||||||
/// </para>
|
/// </para>
|
||||||
/// <para>
|
/// <para>
|
||||||
|
|
@ -33,5 +33,5 @@ public sealed class CompletionsCapability
|
||||||
/// and should return appropriate completion suggestions.
|
/// and should return appropriate completion suggestions.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
[JsonIgnore]
|
[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 ModelContextProtocol.Server;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
|
using System.Security.Claims;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
|
@ -29,28 +30,21 @@ public abstract class JsonRpcMessage
|
||||||
public string JsonRpc { get; init; } = "2.0";
|
public string JsonRpc { get; init; } = "2.0";
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// This is used to support the Streamable HTTP transport where the specification states that the server
|
/// This property contains transport-specific and runtime context information that accompanies
|
||||||
/// SHOULD include JSON-RPC responses in the HTTP response body for the POST request containing
|
/// JSON-RPC messages but is not serialized as part of the JSON-RPC payload. This includes
|
||||||
/// the corresponding JSON-RPC request. It may be <see langword="null"/> for other transports.
|
/// transport references, execution context, and authenticated user information.
|
||||||
/// </remarks>
|
/// </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>
|
/// <remarks>
|
||||||
/// This is used to support the Streamable HTTP transport in its default stateful mode. In this mode,
|
/// This property should only be set when implementing a custom <see cref="ITransport"/>
|
||||||
/// the <see cref="IMcpServer"/> outlives the initial HTTP request context it was created on, and new
|
/// that needs to pass additional per-message context or to pass a <see cref="JsonRpcMessageContext.User"/>
|
||||||
/// JSON-RPC messages can originate from future HTTP requests. This allows the transport to flow the
|
/// to <see cref="StreamableHttpServerTransport.HandlePostRequest(JsonRpcMessage, Stream, CancellationToken)"/>
|
||||||
/// context with the JSON-RPC message. This is particularly useful for enabling IHttpContextAccessor
|
/// or <see cref="SseResponseStreamTransport.OnMessageReceivedAsync(JsonRpcMessage, CancellationToken)"/> .
|
||||||
/// in tool calls.
|
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public ExecutionContext? ExecutionContext { get; set; }
|
public JsonRpcMessageContext? Context { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Provides a <see cref="JsonConverter"/> for <see cref="JsonRpcMessage"/> messages,
|
/// 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>
|
/// <remarks>
|
||||||
/// Requests are messages that require a response from the receiver. Each request includes a unique ID
|
/// 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).
|
/// 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
|
/// 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"/>
|
/// and return either a <see cref="JsonRpcResponse"/> with the result, or a <see cref="JsonRpcError"/>
|
||||||
/// if the method execution fails.
|
/// if the method execution fails.
|
||||||
|
|
@ -36,7 +36,7 @@ public sealed class JsonRpcRequest : JsonRpcMessageWithId
|
||||||
Id = id,
|
Id = id,
|
||||||
Method = Method,
|
Method = Method,
|
||||||
Params = Params,
|
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.
|
/// Gets or sets the handler for set logging level requests from clients.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonIgnore]
|
[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.Nodes;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
using ModelContextProtocol.Server;
|
||||||
|
|
||||||
namespace ModelContextProtocol.Protocol;
|
namespace ModelContextProtocol.Protocol;
|
||||||
|
|
||||||
|
|
@ -59,4 +60,10 @@ public sealed class Prompt : IBaseMetadata
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
[JsonPropertyName("_meta")]
|
[JsonPropertyName("_meta")]
|
||||||
public JsonObject? Meta { get; set; }
|
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.
|
/// Gets or sets whether this server supports notifications for changes to the prompt list.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// When set to <see langword="true"/>, the server will send notifications using
|
/// When set to <see langword="true"/>, the server will send notifications using
|
||||||
/// <see cref="NotificationMethods.PromptListChangedNotification"/> when prompts are added,
|
/// <see cref="NotificationMethods.PromptListChangedNotification"/> when prompts are added,
|
||||||
/// removed, or modified. Clients can register handlers for these notifications to
|
/// 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.
|
/// to available prompts.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
[JsonPropertyName("listChanged")]
|
[JsonPropertyName("listChanged")]
|
||||||
|
|
@ -40,15 +40,15 @@ public sealed class PromptsCapability
|
||||||
/// along with any prompts defined in <see cref="PromptCollection"/>.
|
/// along with any prompts defined in <see cref="PromptCollection"/>.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public Func<RequestContext<ListPromptsRequestParams>, CancellationToken, ValueTask<ListPromptsResult>>? ListPromptsHandler { get; set; }
|
public McpRequestHandler<ListPromptsRequestParams, ListPromptsResult>? ListPromptsHandler { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the handler for <see cref="RequestMethods.PromptsGet"/> requests.
|
/// Gets or sets the handler for <see cref="RequestMethods.PromptsGet"/> requests.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// <para>
|
/// <para>
|
||||||
/// This handler is invoked when a client requests details for a specific prompt by name and provides 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,
|
/// 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.
|
/// and should return a <see cref="GetPromptResult"/> with the prompt messages and other details.
|
||||||
/// </para>
|
/// </para>
|
||||||
/// <para>
|
/// <para>
|
||||||
|
|
@ -57,7 +57,7 @@ public sealed class PromptsCapability
|
||||||
/// </para>
|
/// </para>
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public Func<RequestContext<GetPromptRequestParams>, CancellationToken, ValueTask<GetPromptResult>>? GetPromptHandler { get; set; }
|
public McpRequestHandler<GetPromptRequestParams, GetPromptResult>? GetPromptHandler { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets a collection of prompts that will be served by the server.
|
/// 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:
|
/// when those are provided:
|
||||||
/// </para>
|
/// </para>
|
||||||
/// <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.
|
/// plus any additional prompts provided by the <see cref="ListPromptsHandler"/> if it's set.
|
||||||
/// </para>
|
/// </para>
|
||||||
/// <para>
|
/// <para>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using System.Text.Json.Nodes;
|
using System.Text.Json.Nodes;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
using ModelContextProtocol.Server;
|
||||||
|
|
||||||
namespace ModelContextProtocol.Protocol;
|
namespace ModelContextProtocol.Protocol;
|
||||||
|
|
||||||
|
|
@ -87,4 +88,10 @@ public sealed class Resource : IBaseMetadata
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
[JsonPropertyName("_meta")]
|
[JsonPropertyName("_meta")]
|
||||||
public JsonObject? Meta { get; init; }
|
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.Nodes;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
using ModelContextProtocol.Server;
|
||||||
|
|
||||||
namespace ModelContextProtocol.Protocol;
|
namespace ModelContextProtocol.Protocol;
|
||||||
|
|
||||||
|
|
@ -84,6 +85,12 @@ public sealed class ResourceTemplate : IBaseMetadata
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public bool IsTemplated => UriTemplate.Contains('{');
|
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>
|
/// <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>
|
/// <returns>A <see cref="Resource"/> if <see cref="IsTemplated"/> is <see langword="false"/>; otherwise, <see langword="null"/>.</returns>
|
||||||
public Resource? AsResource()
|
public Resource? AsResource()
|
||||||
|
|
@ -102,6 +109,7 @@ public sealed class ResourceTemplate : IBaseMetadata
|
||||||
MimeType = MimeType,
|
MimeType = MimeType,
|
||||||
Annotations = Annotations,
|
Annotations = Annotations,
|
||||||
Meta = Meta,
|
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.
|
/// Gets or sets whether this server supports notifications for changes to the resource list.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// When set to <see langword="true"/>, the server will send notifications using
|
/// When set to <see langword="true"/>, the server will send notifications using
|
||||||
/// <see cref="NotificationMethods.ResourceListChangedNotification"/> when resources are added,
|
/// <see cref="NotificationMethods.ResourceListChangedNotification"/> when resources are added,
|
||||||
/// removed, or modified. Clients can register handlers for these notifications to
|
/// removed, or modified. Clients can register handlers for these notifications to
|
||||||
/// refresh their resource cache.
|
/// refresh their resource cache.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
|
|
@ -39,7 +39,7 @@ public sealed class ResourcesCapability
|
||||||
/// allowing clients to discover available resource types and their access patterns.
|
/// allowing clients to discover available resource types and their access patterns.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public Func<RequestContext<ListResourceTemplatesRequestParams>, CancellationToken, ValueTask<ListResourceTemplatesResult>>? ListResourceTemplatesHandler { get; set; }
|
public McpRequestHandler<ListResourceTemplatesRequestParams, ListResourceTemplatesResult>? ListResourceTemplatesHandler { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the handler for <see cref="RequestMethods.ResourcesList"/> requests.
|
/// 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.
|
/// The implementation should return a <see cref="ListResourcesResult"/> with the matching resources.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public Func<RequestContext<ListResourcesRequestParams>, CancellationToken, ValueTask<ListResourcesResult>>? ListResourcesHandler { get; set; }
|
public McpRequestHandler<ListResourcesRequestParams, ListResourcesResult>? ListResourcesHandler { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the handler for <see cref="RequestMethods.ResourcesRead"/> requests.
|
/// 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.
|
/// its contents in a ReadResourceResult object.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public Func<RequestContext<ReadResourceRequestParams>, CancellationToken, ValueTask<ReadResourceResult>>? ReadResourceHandler { get; set; }
|
public McpRequestHandler<ReadResourceRequestParams, ReadResourceResult>? ReadResourceHandler { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the handler for <see cref="RequestMethods.ResourcesSubscribe"/> requests.
|
/// Gets or sets the handler for <see cref="RequestMethods.ResourcesSubscribe"/> requests.
|
||||||
|
|
@ -74,7 +74,7 @@ public sealed class ResourcesCapability
|
||||||
/// requiring polling.
|
/// requiring polling.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public Func<RequestContext<SubscribeRequestParams>, CancellationToken, ValueTask<EmptyResult>>? SubscribeToResourcesHandler { get; set; }
|
public McpRequestHandler<SubscribeRequestParams, EmptyResult>? SubscribeToResourcesHandler { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the handler for <see cref="RequestMethods.ResourcesUnsubscribe"/> requests.
|
/// Gets or sets the handler for <see cref="RequestMethods.ResourcesUnsubscribe"/> requests.
|
||||||
|
|
@ -85,7 +85,7 @@ public sealed class ResourcesCapability
|
||||||
/// about the specified resource.
|
/// about the specified resource.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public Func<RequestContext<UnsubscribeRequestParams>, CancellationToken, ValueTask<EmptyResult>>? UnsubscribeFromResourcesHandler { get; set; }
|
public McpRequestHandler<UnsubscribeRequestParams, EmptyResult>? UnsubscribeFromResourcesHandler { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets a collection of resources served by the server.
|
/// Gets or sets a collection of resources served by the server.
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Nodes;
|
using System.Text.Json.Nodes;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
using ModelContextProtocol.Server;
|
||||||
|
|
||||||
namespace ModelContextProtocol.Protocol;
|
namespace ModelContextProtocol.Protocol;
|
||||||
|
|
||||||
|
|
@ -43,7 +44,7 @@ public sealed class Tool : IBaseMetadata
|
||||||
/// if an invalid schema is provided.
|
/// if an invalid schema is provided.
|
||||||
/// </para>
|
/// </para>
|
||||||
/// <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
|
/// their types, and which ones are required. This helps AI models understand
|
||||||
/// how to structure their calls to the tool.
|
/// how to structure their calls to the tool.
|
||||||
/// </para>
|
/// </para>
|
||||||
|
|
@ -52,9 +53,9 @@ public sealed class Tool : IBaseMetadata
|
||||||
/// </para>
|
/// </para>
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
[JsonPropertyName("inputSchema")]
|
[JsonPropertyName("inputSchema")]
|
||||||
public JsonElement InputSchema
|
public JsonElement InputSchema
|
||||||
{
|
{
|
||||||
get => field;
|
get => field;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
if (!McpJsonUtilities.IsValidMcpToolSchema(value))
|
if (!McpJsonUtilities.IsValidMcpToolSchema(value))
|
||||||
|
|
@ -114,4 +115,10 @@ public sealed class Tool : IBaseMetadata
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
[JsonPropertyName("_meta")]
|
[JsonPropertyName("_meta")]
|
||||||
public JsonObject? Meta { get; set; }
|
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