From ec6805004c7646c94795524f66bcc1b8c948d05e Mon Sep 17 00:00:00 2001 From: Destrayon Date: Sat, 12 Jul 2025 14:47:39 -0500 Subject: [PATCH 1/2] feat: Add route-aware tool filtering for MCP servers Implements URL-based tool filtering to enable context-specific tool collections for multi-agent systems and specialized agent workflows. Features: - McpServerToolRouteAttribute for method-level route assignment - WithHttpTransportAndRouting() extension for route-aware transport - MapMcpWithRouting() for automatic route discovery and endpoint mapping - RouteAwareToolService for path normalization and route management - Session-based tool filtering preserving resources and prompts Key capabilities: - Global tools (no route attribute) available on all routes - Route-specific tools only accessible on designated endpoints - Multi-route tools available on multiple specified routes - Backward compatibility with existing WithHttpTransport/MapMcp Example usage: [McpServerToolRoute("admin")] // Available at /mcp/admin [McpServerToolRoute("weather", "utilities")] // Multi-route tool Use cases: - Multi-agent system coordination with specialized tool sets - Agent workflow orchestration with phase-appropriate capabilities - Context-aware tool separation for different agent types - Secure agent collaboration through filtered tool access Tests: Comprehensive test suite covering routing logic, edge cases, and backward compatibility scenarios --- ModelContextProtocol.slnx | 1 + samples/UrlRoutingSseServer/Program.cs | 31 +++ .../Properties/launchSettings.json | 23 ++ samples/UrlRoutingSseServer/README.md | 133 ++++++++++ .../UrlRoutingSseServer/Tools/AdminTool.cs | 22 ++ samples/UrlRoutingSseServer/Tools/EchoTool.cs | 25 ++ samples/UrlRoutingSseServer/Tools/MathTool.cs | 32 +++ .../Tools/SampleLlmTool.cs | 36 +++ .../UrlRoutingSseServer/Tools/WeatherTool.cs | 26 ++ .../UrlRoutingSseServer.csproj | 21 ++ .../appsettings.Development.json | 8 + samples/UrlRoutingSseServer/appsettings.json | 9 + .../McpRouteAwarenessExtension.cs | 229 +++++++++++++++++ .../ModelContextProtocol.AspNetCore.csproj | 4 + .../Services/RouteAwareToolService.cs | 124 +++++++++ .../Server/AIFunctionMcpServerTool.cs | 10 +- .../Server/McpServerTool.cs | 15 ++ .../Server/McpServerToolCreateOptions.cs | 33 +++ .../Server/McpServerToolRouteAttribute.cs | 66 +++++ .../MapMcpRoutingTests.cs | 241 ++++++++++++++++++ 20 files changed, 1087 insertions(+), 2 deletions(-) create mode 100644 samples/UrlRoutingSseServer/Program.cs create mode 100644 samples/UrlRoutingSseServer/Properties/launchSettings.json create mode 100644 samples/UrlRoutingSseServer/README.md create mode 100644 samples/UrlRoutingSseServer/Tools/AdminTool.cs create mode 100644 samples/UrlRoutingSseServer/Tools/EchoTool.cs create mode 100644 samples/UrlRoutingSseServer/Tools/MathTool.cs create mode 100644 samples/UrlRoutingSseServer/Tools/SampleLlmTool.cs create mode 100644 samples/UrlRoutingSseServer/Tools/WeatherTool.cs create mode 100644 samples/UrlRoutingSseServer/UrlRoutingSseServer.csproj create mode 100644 samples/UrlRoutingSseServer/appsettings.Development.json create mode 100644 samples/UrlRoutingSseServer/appsettings.json create mode 100644 src/ModelContextProtocol.AspNetCore/McpRouteAwarenessExtension.cs create mode 100644 src/ModelContextProtocol.AspNetCore/Services/RouteAwareToolService.cs create mode 100644 src/ModelContextProtocol.Core/Server/McpServerToolRouteAttribute.cs create mode 100644 tests/ModelContextProtocol.AspNetCore.Tests/MapMcpRoutingTests.cs diff --git a/ModelContextProtocol.slnx b/ModelContextProtocol.slnx index 5ed8ba0d..c6ab1d42 100644 --- a/ModelContextProtocol.slnx +++ b/ModelContextProtocol.slnx @@ -17,6 +17,7 @@ + diff --git a/samples/UrlRoutingSseServer/Program.cs b/samples/UrlRoutingSseServer/Program.cs new file mode 100644 index 00000000..bf911433 --- /dev/null +++ b/samples/UrlRoutingSseServer/Program.cs @@ -0,0 +1,31 @@ +using OpenTelemetry; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; +using ModelContextProtocol.AspNetCore; +using UrlRoutingSseServer.Tools; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddMcpServer() + .WithHttpTransportAndRouting() + .WithTools() + .WithTools() + .WithTools() + .WithTools() + .WithTools(); + +builder.Services.AddOpenTelemetry() + .WithTracing(b => b.AddSource("*") + .AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation()) + .WithMetrics(b => b.AddMeter("*") + .AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation()) + .WithLogging() + .UseOtlpExporter(); + +var app = builder.Build(); + +app.MapMcpWithRouting("mcp"); + +app.Run(); \ No newline at end of file diff --git a/samples/UrlRoutingSseServer/Properties/launchSettings.json b/samples/UrlRoutingSseServer/Properties/launchSettings.json new file mode 100644 index 00000000..da24c915 --- /dev/null +++ b/samples/UrlRoutingSseServer/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "applicationUrl": "http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "OTEL_SERVICE_NAME": "sse-server", + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "applicationUrl": "https://localhost:7133;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "OTEL_SERVICE_NAME": "sse-server", + } + } + } +} diff --git a/samples/UrlRoutingSseServer/README.md b/samples/UrlRoutingSseServer/README.md new file mode 100644 index 00000000..6ba22205 --- /dev/null +++ b/samples/UrlRoutingSseServer/README.md @@ -0,0 +1,133 @@ +# ASP.NET Core MCP Server with Routing + +This sample demonstrates route-aware tool filtering in MCP servers, allowing different tool sets to be exposed at different HTTP endpoints based on the `[McpServerToolRoute]` attribute. + +## Overview + +The routing feature enables you to: + +- Expose different tools at different HTTP endpoints +- Create context-specific tool collections (admin, utilities, etc.) +- Maintain global tools available on all routes +- Filter tool visibility based on the requested route + +## Route Configuration + +### Available Routes + +| Route | Available Tools | Description | +|-------|----------------|-------------| +| `/mcp` (global) | All tools | Default route with complete tool set | +| `/mcp/admin` | Admin tools + Global tools | Administrative functions | +| `/mcp/weather` | Weather tools + Global tools | Weather-related operations | +| `/mcp/math` | Math tools + Global tools | Mathematical calculations | +| `/mcp/utilities` | Utility tools + Global tools | General utility functions | +| `/mcp/echo` | Echo tools + Global tools | Echo and text operations | + +### Tool Categories + +- **Global Tools**: `SampleLLM` (available on all routes) +- **Admin Tools**: `GetSystemStatus`, `RestartService` (admin route only) +- **Weather Tools**: `GetWeather`, `GetForecast` (weather + utilities routes) +- **Math Tools**: `Add`, `Factorial` (math + utilities routes) +- **Echo Tools**: `Echo`, `EchoAdvanced` (echo + utilities routes) + +## Running the Sample + +1. Start the server: + ```bash + cd samples/UrlRoutingSseServer + dotnet run + ``` + +2. The server will start at `http://localhost:5000` (or port shown in console) + +## Testing Different Routes + +You can test the routing behavior using curl or any HTTP client: + +### List All Tools (Global Route) +```bash +curl -X POST http://localhost:5000/mcp \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' +``` + +### List Admin Tools Only +```bash +curl -X POST http://localhost:5000/mcp/admin \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' +``` + +### List Weather Tools Only +```bash +curl -X POST http://localhost:5000/mcp/weather \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' +``` + +### Call a Tool +```bash +curl -X POST http://localhost:5000/mcp/weather \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"get_weather","arguments":{"city":"Seattle"}}}' +``` + +## Expected Results + +- **Global route** (`/mcp`): Returns all 9 tools +- **Admin route** (`/mcp/admin`): Returns 3 tools (2 admin + 1 global) +- **Weather route** (`/mcp/weather`): Returns 3 tools (2 weather + 1 global) +- **Math route** (`/mcp/math`): Returns 3 tools (2 math + 1 global) +- **Utilities route** (`/mcp/utilities`): Returns 4 tools (3 utility + 1 global) + +## Implementation Details + +### Key Configuration Changes + +The routing feature requires two configuration changes: + +```csharp +// Use routing-enabled transport +builder.Services.AddMcpServer() + .WithHttpTransportAndRouting() // Instead of .WithHttpTransport() + +// Map with routing support +app.MapMcpWithRouting("mcp"); // Instead of app.MapMcp() +``` + +### Route Attribute Usage + +```csharp +[McpServerTool, Description("Admin-only tool")] +[McpServerToolRoute("admin")] // Single route +public static string AdminTool() { ... } + +[McpServerTool, Description("Multi-route tool")] +[McpServerToolRoute("weather", "utilities")] // Multiple routes +public static string UtilityTool() { ... } + +[McpServerTool, Description("Global tool")] +// No [McpServerToolRoute] = available everywhere +public static string GlobalTool() { ... } +``` + +## Use Cases + +This routing feature enables scenarios like: + +- **Multi-agent system coordination**: Different agent types access specialized tool sets (research agents get web search, execution agents get file operations) +- **Context-aware tool separation**: Specialized agents with distinct purposes and capabilities working within the same MCP server +- **Agent workflow orchestration**: Route-specific tools for different phases of multi-step agent workflows +- **Specialized agent environments**: Domain-specific agents (coding, research, planning) each with their appropriate toolset +- **Agent collaboration patterns**: Enabling agent-to-agent handoffs with context-appropriate tools at each stage + +## Key Files + +- `Program.cs`: Server configuration with routing enabled +- `Tools/AdminTool.cs`: Administrative tools (admin route only) +- `Tools/EchoTool.cs`: Basic echo tools with route filtering +- `Tools/MathTool.cs`: Mathematical calculation tools +- `Tools/SampleLlmTool.cs`: Global tool (no route restriction) +- `Tools/WeatherTool.cs`: Weather-related tools \ No newline at end of file diff --git a/samples/UrlRoutingSseServer/Tools/AdminTool.cs b/samples/UrlRoutingSseServer/Tools/AdminTool.cs new file mode 100644 index 00000000..f53cfbc6 --- /dev/null +++ b/samples/UrlRoutingSseServer/Tools/AdminTool.cs @@ -0,0 +1,22 @@ +using ModelContextProtocol.Server; +using System.ComponentModel; + +namespace UrlRoutingSseServer.Tools; + +[McpServerToolType] +public sealed class AdminTool +{ + [McpServerTool, Description("Gets system status information - admin only.")] + [McpServerToolRoute("admin")] + public static string GetSystemStatus() + { + return $"System Status: Running | Uptime: {Environment.TickCount64 / 1000}s | Memory: {GC.GetTotalMemory(false) / 1024 / 1024}MB"; + } + + [McpServerTool, Description("Restarts a service - admin only.")] + [McpServerToolRoute("admin")] + public static string RestartService([Description("Name of the service to restart")] string serviceName) + { + return $"Service '{serviceName}' restart initiated (simulated)"; + } +} \ No newline at end of file diff --git a/samples/UrlRoutingSseServer/Tools/EchoTool.cs b/samples/UrlRoutingSseServer/Tools/EchoTool.cs new file mode 100644 index 00000000..2aeaf2a0 --- /dev/null +++ b/samples/UrlRoutingSseServer/Tools/EchoTool.cs @@ -0,0 +1,25 @@ +using ModelContextProtocol.Server; +using System.ComponentModel; + +namespace UrlRoutingSseServer.Tools; + +[McpServerToolType] +public sealed class EchoTool +{ + [McpServerTool, Description("Echoes the input back to the client.")] + [McpServerToolRoute("echo")] + public static string Echo(string message) + { + return "hello " + message; + } + + [McpServerTool(Name = "echo_advanced"), Description("Advanced echo with formatting options.")] + [McpServerToolRoute("echo", "utilities")] + public static string EchoAdvanced( + [Description("The message to echo")] string message, + [Description("Whether to convert to uppercase")] bool uppercase = false) + { + var result = $"Advanced echo: {message}"; + return uppercase ? result.ToUpper() : result; + } +} diff --git a/samples/UrlRoutingSseServer/Tools/MathTool.cs b/samples/UrlRoutingSseServer/Tools/MathTool.cs new file mode 100644 index 00000000..c01fc381 --- /dev/null +++ b/samples/UrlRoutingSseServer/Tools/MathTool.cs @@ -0,0 +1,32 @@ +using ModelContextProtocol.Server; +using System.ComponentModel; + +namespace UrlRoutingSseServer.Tools; + +[McpServerToolType] +public sealed class MathTool +{ + [McpServerTool, Description("Adds two numbers together.")] + [McpServerToolRoute("math", "utilities")] + public static int Add( + [Description("First number")] int a, + [Description("Second number")] int b) + { + return a + b; + } + + [McpServerTool, Description("Calculates factorial of a number.")] + [McpServerToolRoute("math")] + public static long Factorial([Description("Number to calculate factorial for")] int n) + { + if (n < 0) return -1; + if (n == 0 || n == 1) return 1; + + long result = 1; + for (int i = 2; i <= n; i++) + { + result *= i; + } + return result; + } +} diff --git a/samples/UrlRoutingSseServer/Tools/SampleLlmTool.cs b/samples/UrlRoutingSseServer/Tools/SampleLlmTool.cs new file mode 100644 index 00000000..8a75e9f1 --- /dev/null +++ b/samples/UrlRoutingSseServer/Tools/SampleLlmTool.cs @@ -0,0 +1,36 @@ +using Microsoft.Extensions.AI; +using ModelContextProtocol.Server; +using System.ComponentModel; + +namespace UrlRoutingSseServer.Tools; + +/// +/// This tool uses dependency injection and async method +/// +[McpServerToolType] +public sealed class SampleLlmTool +{ + [McpServerTool(Name = "sampleLLM"), Description("Samples from an LLM using MCP's sampling feature")] + public static async Task SampleLLM( + IMcpServer thisServer, + [Description("The prompt to send to the LLM")] string prompt, + [Description("Maximum number of tokens to generate")] int maxTokens, + CancellationToken cancellationToken) + { + ChatMessage[] messages = + [ + new(ChatRole.System, "You are a helpful test server."), + new(ChatRole.User, prompt), + ]; + + ChatOptions options = new() + { + MaxOutputTokens = maxTokens, + Temperature = 0.7f, + }; + + var samplingResponse = await thisServer.AsSamplingChatClient().GetResponseAsync(messages, options, cancellationToken); + + return $"LLM sampling result: {samplingResponse}"; + } +} diff --git a/samples/UrlRoutingSseServer/Tools/WeatherTool.cs b/samples/UrlRoutingSseServer/Tools/WeatherTool.cs new file mode 100644 index 00000000..60d191b0 --- /dev/null +++ b/samples/UrlRoutingSseServer/Tools/WeatherTool.cs @@ -0,0 +1,26 @@ +using ModelContextProtocol.Server; +using System.ComponentModel; + +namespace UrlRoutingSseServer.Tools; + +[McpServerToolType] +public sealed class WeatherTool +{ + [McpServerTool, Description("Gets current weather for a location.")] + [McpServerToolRoute("weather", "utilities")] + public static string GetWeather([Description("City name")] string city) + { + var temps = new[] { 72, 68, 75, 80, 77 }; + var conditions = new[] { "Sunny", "Cloudy", "Rainy", "Partly Cloudy", "Clear" }; + var random = new Random(city.GetHashCode()); // Deterministic based on city + + return $"Weather in {city}: {temps[random.Next(temps.Length)]}°F, {conditions[random.Next(conditions.Length)]}"; + } + + [McpServerTool, Description("Gets 5-day weather forecast.")] + [McpServerToolRoute("weather")] + public static string GetForecast([Description("City name")] string city) + { + return $"5-day forecast for {city}: Mon 75°F, Tue 73°F, Wed 78°F, Thu 72°F, Fri 76°F"; + } +} diff --git a/samples/UrlRoutingSseServer/UrlRoutingSseServer.csproj b/samples/UrlRoutingSseServer/UrlRoutingSseServer.csproj new file mode 100644 index 00000000..960d9721 --- /dev/null +++ b/samples/UrlRoutingSseServer/UrlRoutingSseServer.csproj @@ -0,0 +1,21 @@ + + + + net9.0 + enable + enable + true + + + + + + + + + + + + + + diff --git a/samples/UrlRoutingSseServer/appsettings.Development.json b/samples/UrlRoutingSseServer/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/samples/UrlRoutingSseServer/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/samples/UrlRoutingSseServer/appsettings.json b/samples/UrlRoutingSseServer/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/samples/UrlRoutingSseServer/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/src/ModelContextProtocol.AspNetCore/McpRouteAwarenessExtension.cs b/src/ModelContextProtocol.AspNetCore/McpRouteAwarenessExtension.cs new file mode 100644 index 00000000..13f645d9 --- /dev/null +++ b/src/ModelContextProtocol.AspNetCore/McpRouteAwarenessExtension.cs @@ -0,0 +1,229 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; +using ModelContextProtocol.AspNetCore.Services; +using ModelContextProtocol.Server; +using System.Diagnostics.CodeAnalysis; + +namespace ModelContextProtocol.AspNetCore; + +/// +/// Provides extension methods for configuring MCP servers with route-aware tool filtering capabilities. +/// +/// +/// +/// This class extends the standard MCP configuration to support route-based tool filtering, +/// allowing different tool sets to be exposed at different URL paths based on the +/// applied to tool methods. +/// +/// +/// Route-aware filtering enables scenarios where different contexts require different +/// tool capabilities, such as admin-only routes, read-only endpoints, or domain-specific +/// tool collections. +/// +/// +public static class McpRouteAwarenessExtension +{ + /// + /// Configures the MCP server with HTTP transport and route-aware tool filtering capabilities. + /// + /// The MCP server builder to configure. + /// Optional action to configure additional HTTP transport options. + /// The configured for method chaining. + /// + /// + /// This method enhances the standard HTTP transport with route-aware session configuration + /// that filters available tools based on the requested URL path. Tools without route + /// attributes are considered global and available on all routes. + /// + /// + /// The filtering is applied per-session, ensuring that each client connection only + /// sees tools appropriate for the requested route. + /// + /// + /// Thrown when is null. + public static IMcpServerBuilder WithHttpTransportAndRouting(this IMcpServerBuilder builder, Action? configureOptions = null) + { + if (builder is null) + throw new ArgumentNullException(nameof(builder)); + + // Configure HTTP transport with route-aware session options + builder.WithHttpTransport(options => + { + // Set up the route-aware session configuration + options.ConfigureSessionOptions = async (httpContext, mcpOptions, cancellationToken) => + { + var routeService = httpContext.RequestServices.GetService(); + var requestPath = NormalizePath(httpContext.Request.Path.Value); + + // If no route service or requesting the global route, serve all tools + if (routeService?.GlobalRoute is null || string.Equals(requestPath, routeService.GlobalRoute, StringComparison.OrdinalIgnoreCase)) + { + return; + } + + var toolCollection = mcpOptions.Capabilities?.Tools?.ToolCollection; + if (toolCollection is null) + { + return; + } + + // Create a snapshot of current tools before filtering + var allTools = toolCollection.ToList(); + toolCollection.Clear(); + + // Filter tools based on the requested route + foreach (var tool in allTools) + { + if (ShouldIncludeTool(tool, requestPath, routeService)) + { + toolCollection.Add(tool); + } + } + + // Note: Resources and prompts are not filtered - they remain available on all routes + }; + + // Apply any additional configuration + configureOptions?.Invoke(options); + }); + + // Register the route service as a singleton + builder.Services.AddSingleton(); + + return builder; + } + + /// + /// Maps MCP endpoints with route-aware capabilities, creating endpoints for both the global route and all discovered tool routes. + /// + /// The endpoint route builder to configure. + /// The route pattern prefix for the global MCP endpoint. + /// An for configuring additional endpoint conventions. + /// + /// + /// This method discovers all tool routes from registered instances + /// and creates MCP endpoints for each unique route. Tools without route attributes are + /// available on the global route and all specific routes. + /// + /// + /// For example, with a global pattern of "mcp" and tools having routes "admin" and "readonly": + /// + /// /mcp - serves all tools (global route) + /// /mcp/admin - serves admin-specific tools + global tools + /// /mcp/readonly - serves readonly-specific tools + global tools + /// + /// + /// + /// Thrown when is null. + public static IEndpointConventionBuilder MapMcpWithRouting(this IEndpointRouteBuilder endpoints, [StringSyntax("Route")] string pattern = "") + { + if (endpoints is null) + throw new ArgumentNullException(nameof(endpoints)); + + var tools = endpoints.ServiceProvider.GetServices(); + var routeService = endpoints.ServiceProvider.GetService(); + + // Fallback to standard mapping if route service is not available + if (routeService is null) + { + return endpoints.MapMcp(pattern); + } + + // Normalize and register the global route + var normalizedGlobalRoute = NormalizePath(pattern); + routeService.RegisterGlobalRoute(normalizedGlobalRoute); + + // Discover and register all tool-specific routes + foreach (var tool in tools) + { + if (tool.Routes?.Count > 0) + { + var toolRoutes = tool.Routes + .Select(route => NormalizePath($"{normalizedGlobalRoute}/{route}")) + .ToArray(); + + routeService.RegisterOtherRoutes(toolRoutes); + } + } + + // Map MCP endpoints for all discovered routes + foreach (var discoveredRoute in routeService.OtherRoutes) + { + endpoints.MapMcp(discoveredRoute); + } + + // Map the global route and return its convention builder + return endpoints.MapMcp(normalizedGlobalRoute); + } + + /// + /// Determines whether a tool should be included in the filtered tool collection for the specified route. + /// + /// The tool to evaluate for inclusion. + /// The requested URL path (normalized). + /// The route service containing route information. + /// if the tool should be included; otherwise, . + /// + /// A tool is included if: + /// + /// It has no route attributes (global tool), OR + /// One of its routes matches the requested path + /// + /// + private static bool ShouldIncludeTool(McpServerTool tool, string requestPath, RouteAwareToolService routeService) + { + // Global tools (no routes) are always included + if (tool.Routes is null || tool.Routes.Count == 0) + { + return true; + } + + // Check if any of the tool's routes match the requested path + return tool.Routes.Any(route => + { + var fullToolPath = NormalizePath($"{routeService.GlobalRoute}/{route}"); + return string.Equals(fullToolPath, requestPath, StringComparison.OrdinalIgnoreCase); + }); + } + + /// + /// Normalizes a path to ensure consistent formatting for route comparison. + /// + /// The path to normalize. + /// A normalized path with consistent formatting. + /// + /// This method applies the same normalization rules as + /// to ensure consistent path handling throughout the routing system. + /// + private static string NormalizePath(string? path) + { + if (string.IsNullOrWhiteSpace(path)) + { + return "/"; + } + + // Trim whitespace + path = path.Trim(); + + // Ensure it starts with a slash + if (!path.StartsWith('/')) + { + path = "/" + path; + } + + // Remove trailing slashes (except for root "/") + if (path.Length > 1 && path.EndsWith('/')) + { + path = path.TrimEnd('/'); + } + + // Handle multiple consecutive slashes + while (path.Contains("//")) + { + path = path.Replace("//", "/"); + } + + return path; + } +} \ No newline at end of file diff --git a/src/ModelContextProtocol.AspNetCore/ModelContextProtocol.AspNetCore.csproj b/src/ModelContextProtocol.AspNetCore/ModelContextProtocol.AspNetCore.csproj index 7968e23f..ab136b88 100644 --- a/src/ModelContextProtocol.AspNetCore/ModelContextProtocol.AspNetCore.csproj +++ b/src/ModelContextProtocol.AspNetCore/ModelContextProtocol.AspNetCore.csproj @@ -24,4 +24,8 @@ + + + + diff --git a/src/ModelContextProtocol.AspNetCore/Services/RouteAwareToolService.cs b/src/ModelContextProtocol.AspNetCore/Services/RouteAwareToolService.cs new file mode 100644 index 00000000..581dabae --- /dev/null +++ b/src/ModelContextProtocol.AspNetCore/Services/RouteAwareToolService.cs @@ -0,0 +1,124 @@ +using Microsoft.Extensions.Options; +using ModelContextProtocol.Protocol; +using ModelContextProtocol.Server; + +namespace ModelContextProtocol.AspNetCore.Services; + +/// +/// Service that handles route-aware tool filtering and automatic route discovery. +/// +/// +/// +/// This service maintains a registry of all available routes in the MCP server, +/// including the global route that serves all tools and specific routes that +/// serve filtered tool sets based on the . +/// +/// +/// Routes are normalized to ensure consistent formatting and case-insensitive +/// comparison for reliable matching during request processing. +/// +/// +public class RouteAwareToolService +{ + private readonly HashSet _otherRoutes = new(StringComparer.OrdinalIgnoreCase); + + /// + /// Gets or sets the base route for the MCP server. + /// + /// + /// The global route serves all available tools regardless of their route attributes. + /// Multiple calls to will overwrite the previous value. + /// + public string? GlobalRoute { get; private set; } + + /// + /// Gets all discovered routes from tool attributes. + /// + /// + /// This collection contains all routes discovered from + /// applied to tool methods, normalized for consistent comparison. + /// + public IReadOnlySet OtherRoutes => _otherRoutes; + + /// + /// Registers routes with their associated tools. + /// + /// Array of route paths to register. Null values are ignored. + /// + /// Routes are automatically normalized to ensure consistent formatting. + /// Duplicate routes are ignored due to the underlying implementation. + /// + public void RegisterOtherRoutes(string[]? otherRoutes) + { + if (otherRoutes == null) + return; + + foreach (string route in otherRoutes) + { + var normalizedRoute = NormalizePath(route); + _otherRoutes.Add(normalizedRoute); + } + } + + /// + /// Registers the global route that serves all tools. + /// + /// The global route path. + /// + /// Multiple calls to this method will overwrite the previous global route value, + /// following Microsoft's convention of "last registration wins". + /// + /// Thrown when is null or whitespace. + public void RegisterGlobalRoute(string globalRoute) + { + if (string.IsNullOrWhiteSpace(globalRoute)) + throw new ArgumentException("Global route cannot be null or empty.", nameof(globalRoute)); + + GlobalRoute = NormalizePath(globalRoute); + } + + /// + /// Normalizes a path to ensure it follows consistent formatting rules. + /// + /// The path to normalize. + /// A normalized path with consistent formatting. + /// + /// Normalization rules: + /// + /// Null or whitespace paths become "/" + /// Ensures paths start with "/" + /// Removes trailing slashes except for root "/" + /// Collapses multiple consecutive slashes to single slash + /// + /// + private static string NormalizePath(string? path) + { + if (string.IsNullOrWhiteSpace(path)) + { + return "/"; + } + + // Trim whitespace + path = path.Trim(); + + // Ensure it starts with a slash + if (!path.StartsWith('/')) + { + path = "/" + path; + } + + // Remove trailing slashes (except for root "/") + if (path.Length > 1 && path.EndsWith('/')) + { + path = path.TrimEnd('/'); + } + + // Handle multiple consecutive slashes + while (path.Contains("//")) + { + path = path.Replace("//", "/"); + } + + return path; + } +} \ No newline at end of file diff --git a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs index afd3912b..d0c4db06 100644 --- a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs +++ b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs @@ -146,7 +146,7 @@ options.OpenWorld is not null || } } - return new AIFunctionMcpServerTool(function, tool, options?.Services, structuredOutputRequiresWrapping); + return new AIFunctionMcpServerTool(function, tool, options?.Services, structuredOutputRequiresWrapping, options?.GetRoutes()); } private static McpServerToolCreateOptions DeriveOptions(MethodInfo method, McpServerToolCreateOptions? options) @@ -186,6 +186,11 @@ private static McpServerToolCreateOptions DeriveOptions(MethodInfo method, McpSe newOptions.Description ??= descAttr.Description; } + if (method.GetCustomAttribute() is { } routeAttr) + { + newOptions.SetRoutes(routeAttr.Routes); + } + return newOptions; } @@ -193,12 +198,13 @@ private static McpServerToolCreateOptions DeriveOptions(MethodInfo method, McpSe internal AIFunction AIFunction { get; } /// Initializes a new instance of the class. - private AIFunctionMcpServerTool(AIFunction function, Tool tool, IServiceProvider? serviceProvider, bool structuredOutputRequiresWrapping) + private AIFunctionMcpServerTool(AIFunction function, Tool tool, IServiceProvider? serviceProvider, bool structuredOutputRequiresWrapping, IReadOnlyList? routes = null) { AIFunction = function; ProtocolTool = tool; _logger = serviceProvider?.GetService()?.CreateLogger() ?? (ILogger)NullLogger.Instance; _structuredOutputRequiresWrapping = structuredOutputRequiresWrapping; + Routes = routes; } /// diff --git a/src/ModelContextProtocol.Core/Server/McpServerTool.cs b/src/ModelContextProtocol.Core/Server/McpServerTool.cs index e3958271..21d645e0 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerTool.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerTool.cs @@ -141,6 +141,21 @@ protected McpServerTool() /// Gets the protocol type for this instance. public abstract Tool ProtocolTool { get; } + /// + /// Gets the HTTP routes where this tool should be available. + /// + /// + /// + /// If , the tool is available on all routes (global tool). + /// Global tools are always accessible regardless of the specific route requested. + /// + /// + /// This property is used in AspNetCore scenarios to control which HTTP endpoints + /// expose this tool. It is ignored in stdio/console scenarios. + /// + /// + public IReadOnlyList? Routes { get; protected set; } + /// Invokes the . /// The request information resulting in the invocation of this tool. /// The to monitor for cancellation requests. The default is . diff --git a/src/ModelContextProtocol.Core/Server/McpServerToolCreateOptions.cs b/src/ModelContextProtocol.Core/Server/McpServerToolCreateOptions.cs index bdb4ecb8..15b8ffb0 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerToolCreateOptions.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerToolCreateOptions.cs @@ -64,6 +64,38 @@ public sealed class McpServerToolCreateOptions /// public string? Title { get; set; } + private IReadOnlyList? routes; + + /// + /// Gets the HTTP routes where this tool should be available. + /// + /// + /// An array of route names, or if the tool should be available on all routes (global tool). + /// + /// + /// Routes are used in AspNetCore scenarios to control which HTTP endpoints expose this tool. + /// This setting is ignored in stdio/console scenarios. + /// + public IReadOnlyList? GetRoutes() + { + return routes; + } + + /// + /// Sets the HTTP routes where this tool should be available. + /// + /// + /// An array of route names, or to make the tool available on all routes (global tool). + /// + /// + /// Routes are typically populated from during tool creation, + /// but can be set programmatically when creating tools without attributes. + /// + public void SetRoutes(IReadOnlyList? value) + { + routes = value; + } + /// /// Gets or sets whether the tool may perform destructive updates to its environment. /// @@ -172,5 +204,6 @@ internal McpServerToolCreateOptions Clone() => UseStructuredContent = UseStructuredContent, SerializerOptions = SerializerOptions, SchemaCreateOptions = SchemaCreateOptions, + routes = routes }; } diff --git a/src/ModelContextProtocol.Core/Server/McpServerToolRouteAttribute.cs b/src/ModelContextProtocol.Core/Server/McpServerToolRouteAttribute.cs new file mode 100644 index 00000000..2be96324 --- /dev/null +++ b/src/ModelContextProtocol.Core/Server/McpServerToolRouteAttribute.cs @@ -0,0 +1,66 @@ +using System; +using System.Linq; + +namespace ModelContextProtocol.Server; + +/// +/// Specifies which HTTP route(s) this MCP tool should be available on. +/// This attribute is only used in AspNetCore scenarios and is ignored in stdio/console scenarios. +/// +/// +/// +/// This attribute works alongside the to provide +/// HTTP routing capabilities. Tool methods can have both routing and core tool attributes +/// to control where they are accessible via HTTP endpoints. +/// +/// +/// Tools without this attribute are considered global and available on all routes. +/// Multiple routes can be specified in a single attribute or by applying the attribute multiple times. +/// +/// +/// Example usage: +/// +/// [McpServerTool, Description("Echoes the input back to the client.")] +/// [McpServerToolRoute("echo")] +/// public static string Echo(string message) { ... } +/// +/// // Tool available on multiple routes +/// [McpServerTool, Description("Gets weather data")] +/// [McpServerToolRoute("weather", "utilities")] +/// public static string GetWeather(string location) { ... } +/// +/// // Global tool (available everywhere) +/// [McpServerTool, Description("System diagnostics")] +/// public static string GetDiagnostics() { ... } +/// +/// +/// +[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] +public sealed class McpServerToolRouteAttribute : Attribute +{ + /// + /// Gets the route names this tool should be available on. + /// + /// + /// Route names are case-insensitive and will be matched against HTTP route segments. + /// For example, a route name "echo" will be accessible at /mcp/echo when the global + /// route is configured as "/mcp". + /// + public IReadOnlyList Routes { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The route names this tool should be available on. + /// Thrown when no routes are specified or any route is null/empty. + public McpServerToolRouteAttribute(params string[] routes) + { + if (routes == null || routes.Length == 0) + throw new ArgumentException("At least one route must be specified", nameof(routes)); + + if (routes.Any(route => string.IsNullOrWhiteSpace(route))) + throw new ArgumentException("Route names cannot be null or empty", nameof(routes)); + + Routes = routes.Select(route => route.Trim()).ToArray(); + } +} \ No newline at end of file diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/MapMcpRoutingTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/MapMcpRoutingTests.cs new file mode 100644 index 00000000..c55f4586 --- /dev/null +++ b/tests/ModelContextProtocol.AspNetCore.Tests/MapMcpRoutingTests.cs @@ -0,0 +1,241 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using ModelContextProtocol.Client; +using ModelContextProtocol.Protocol; +using ModelContextProtocol.Server; +using System.ComponentModel; + +namespace ModelContextProtocol.AspNetCore.Tests; + +public class MapMcpRoutingTests(ITestOutputHelper outputHelper) : MapMcpTests(outputHelper) +{ + protected override bool UseStreamableHttp => true; + protected override bool Stateless => false; + + [Fact] + public async Task WithHttpTransportAndRouting_ThrowsInvalidOperationException_IfWithHttpTransportIsNotCalled() + { + Builder.Services.AddMcpServer(); + await using var app = Builder.Build(); + var exception = Assert.Throws(() => app.MapMcpWithRouting()); + Assert.Contains("WithHttpTransport", exception.Message); + } + + [Theory] + [InlineData("/mcp", 4)] // Global route - all tools (global + admin + weather + multi_route) + [InlineData("/mcp/admin", 2)] // Admin route - admin tools + global tools + [InlineData("/mcp/weather", 3)] // Weather route - weather + multi_route + global tools + [InlineData("/mcp/math", 2)] // Math route - multi_route + global tools + public async Task Route_Filtering_ReturnsCorrectToolCount(string route, int expectedToolCount) + { + Builder.Services.AddMcpServer() + .WithHttpTransportAndRouting() + .WithTools(); + + await using var app = Builder.Build(); + app.MapMcpWithRouting("mcp"); + await app.StartAsync(TestContext.Current.CancellationToken); + + await using var mcpClient = await ConnectAsync(route); + var tools = await mcpClient.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken); + + Assert.Equal(expectedToolCount, tools.Count); + } + + [Fact] + public async Task GlobalRoute_ReturnsAllTools() + { + Builder.Services.AddMcpServer() + .WithHttpTransportAndRouting() + .WithTools(); + + await using var app = Builder.Build(); + app.MapMcpWithRouting("mcp"); + await app.StartAsync(TestContext.Current.CancellationToken); + + await using var mcpClient = await ConnectAsync("/mcp"); + var tools = await mcpClient.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken); + + var toolNames = tools.Select(t => t.Name).ToHashSet(); + Assert.Contains("global_tool", toolNames); + Assert.Contains("admin_tool", toolNames); + Assert.Contains("weather_tool", toolNames); + } + + [Fact] + public async Task AdminRoute_OnlyReturnsAdminAndGlobalTools() + { + Builder.Services.AddMcpServer() + .WithHttpTransportAndRouting() + .WithTools(); + + await using var app = Builder.Build(); + app.MapMcpWithRouting("mcp"); + await app.StartAsync(TestContext.Current.CancellationToken); + + await using var mcpClient = await ConnectAsync("/mcp/admin"); + var tools = await mcpClient.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken); + + var toolNames = tools.Select(t => t.Name).ToHashSet(); + Assert.Contains("global_tool", toolNames); + Assert.Contains("admin_tool", toolNames); + Assert.DoesNotContain("weather_tool", toolNames); + } + + [Fact] + public async Task WeatherRoute_OnlyReturnsWeatherAndGlobalTools() + { + Builder.Services.AddMcpServer() + .WithHttpTransportAndRouting() + .WithTools(); + + await using var app = Builder.Build(); + app.MapMcpWithRouting("mcp"); + await app.StartAsync(TestContext.Current.CancellationToken); + + await using var mcpClient = await ConnectAsync("/mcp/weather"); + var tools = await mcpClient.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken); + + var toolNames = tools.Select(t => t.Name).ToHashSet(); + Assert.Contains("global_tool", toolNames); + Assert.Contains("weather_tool", toolNames); + Assert.DoesNotContain("admin_tool", toolNames); + } + + [Theory] + [InlineData("/mcp/")] + [InlineData("/mcp")] + public async Task TrailingSlash_HandledCorrectly_ForGlobalRoute(string route) + { + Builder.Services.AddMcpServer() + .WithHttpTransportAndRouting() + .WithTools(); + + await using var app = Builder.Build(); + app.MapMcpWithRouting("mcp"); + await app.StartAsync(TestContext.Current.CancellationToken); + + await using var mcpClient = await ConnectAsync(route); + var tools = await mcpClient.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken); + + // Should return all tools (global route behavior) + Assert.Equal(4, tools.Count); + } + + [Theory] + [InlineData("/mcp/admin")] + [InlineData("/mcp/admin/")] + public async Task TrailingSlash_HandledCorrectly_ForSpecificRoute(string route) + { + Builder.Services.AddMcpServer() + .WithHttpTransportAndRouting() + .WithTools(); + + await using var app = Builder.Build(); + app.MapMcpWithRouting("mcp"); + await app.StartAsync(TestContext.Current.CancellationToken); + + await using var mcpClient = await ConnectAsync(route); + var tools = await mcpClient.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken); + + // Should return admin + global tools only + Assert.Equal(2, tools.Count); + var toolNames = tools.Select(t => t.Name).ToHashSet(); + Assert.Contains("admin_tool", toolNames); + Assert.Contains("global_tool", toolNames); + } + + [Fact] + public async Task MultiRouteTools_AppearOnAllSpecifiedRoutes() + { + Builder.Services.AddMcpServer() + .WithHttpTransportAndRouting() + .WithTools(); + + await using var app = Builder.Build(); + app.MapMcpWithRouting("mcp"); + await app.StartAsync(TestContext.Current.CancellationToken); + + // Check math route + await using var mathClient = await ConnectAsync("/mcp/math"); + var mathTools = await mathClient.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken); + var mathToolNames = mathTools.Select(t => t.Name).ToHashSet(); + Assert.Contains("multi_route_tool", mathToolNames); + + // Check weather route + await using var weatherClient = await ConnectAsync("/mcp/weather"); + var weatherTools = await weatherClient.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken); + var weatherToolNames = weatherTools.Select(t => t.Name).ToHashSet(); + Assert.Contains("multi_route_tool", weatherToolNames); + } + + [Fact] + public async Task ToolInvocation_WorksOnCorrectRoute() + { + Builder.Services.AddMcpServer() + .WithHttpTransportAndRouting() + .WithTools(); + + await using var app = Builder.Build(); + app.MapMcpWithRouting("mcp"); + await app.StartAsync(TestContext.Current.CancellationToken); + + await using var mcpClient = await ConnectAsync("/mcp/admin"); + var result = await mcpClient.CallToolAsync( + "admin_tool", + new Dictionary(), + cancellationToken: TestContext.Current.CancellationToken); + + var content = Assert.Single(result.Content.OfType()); + Assert.Equal("Admin tool executed", content.Text); + } + + [Fact] + public async Task BackwardCompatibility_StandardMapMcp_StillWorks() + { + Builder.Services.AddMcpServer() + .WithHttpTransport() + .WithTools(); + + await using var app = Builder.Build(); + app.MapMcp("mcp"); + await app.StartAsync(TestContext.Current.CancellationToken); + + await using var mcpClient = await ConnectAsync("/mcp"); + var tools = await mcpClient.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken); + + // Should return all tools when not using routing + Assert.Equal(4, tools.Count); // All tools including multi-route + } +} + +[McpServerToolType] +public sealed class RoutingTestTools +{ + [McpServerTool(Name = "global_tool"), Description("Global tool available on all routes")] + public static string GlobalTool() + { + return "Global tool executed"; + } + + [McpServerTool(Name = "admin_tool"), Description("Admin-only tool")] + [McpServerToolRoute("admin")] + public static string AdminTool() + { + return "Admin tool executed"; + } + + [McpServerTool(Name = "weather_tool"), Description("Weather-specific tool")] + [McpServerToolRoute("weather")] + public static string WeatherTool() + { + return "Weather tool executed"; + } + + [McpServerTool(Name = "multi_route_tool"), Description("Tool available on multiple routes")] + [McpServerToolRoute("math", "weather")] + public static string MultiRouteTool() + { + return "Multi-route tool executed"; + } +} \ No newline at end of file From a93b43842d8136704b646b624ea07e976399b95b Mon Sep 17 00:00:00 2001 From: Destrayon Date: Tue, 15 Jul 2025 10:13:30 -0500 Subject: [PATCH 2/2] Made some changes to feedback: -Removed Attributes folder -Changed routes getter and setter to a Routes property Change also made for consistency: -Removed Services folder and namespace -Moved RouteAwareToolService.cs out of the folder and namespace -Removed reference to this namespace from McpRouteAwarenessExtension.cs --- .../McpRouteAwarenessExtension.cs | 1 - .../ModelContextProtocol.AspNetCore.csproj | 4 --- .../{Services => }/RouteAwareToolService.cs | 2 +- .../Server/AIFunctionMcpServerTool.cs | 4 +-- .../Server/McpServerToolCreateOptions.cs | 32 +++++-------------- 5 files changed, 11 insertions(+), 32 deletions(-) rename src/ModelContextProtocol.AspNetCore/{Services => }/RouteAwareToolService.cs (98%) diff --git a/src/ModelContextProtocol.AspNetCore/McpRouteAwarenessExtension.cs b/src/ModelContextProtocol.AspNetCore/McpRouteAwarenessExtension.cs index 13f645d9..e6ff2489 100644 --- a/src/ModelContextProtocol.AspNetCore/McpRouteAwarenessExtension.cs +++ b/src/ModelContextProtocol.AspNetCore/McpRouteAwarenessExtension.cs @@ -1,7 +1,6 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; -using ModelContextProtocol.AspNetCore.Services; using ModelContextProtocol.Server; using System.Diagnostics.CodeAnalysis; diff --git a/src/ModelContextProtocol.AspNetCore/ModelContextProtocol.AspNetCore.csproj b/src/ModelContextProtocol.AspNetCore/ModelContextProtocol.AspNetCore.csproj index ab136b88..7968e23f 100644 --- a/src/ModelContextProtocol.AspNetCore/ModelContextProtocol.AspNetCore.csproj +++ b/src/ModelContextProtocol.AspNetCore/ModelContextProtocol.AspNetCore.csproj @@ -24,8 +24,4 @@ - - - - diff --git a/src/ModelContextProtocol.AspNetCore/Services/RouteAwareToolService.cs b/src/ModelContextProtocol.AspNetCore/RouteAwareToolService.cs similarity index 98% rename from src/ModelContextProtocol.AspNetCore/Services/RouteAwareToolService.cs rename to src/ModelContextProtocol.AspNetCore/RouteAwareToolService.cs index 581dabae..64da5179 100644 --- a/src/ModelContextProtocol.AspNetCore/Services/RouteAwareToolService.cs +++ b/src/ModelContextProtocol.AspNetCore/RouteAwareToolService.cs @@ -2,7 +2,7 @@ using ModelContextProtocol.Protocol; using ModelContextProtocol.Server; -namespace ModelContextProtocol.AspNetCore.Services; +namespace ModelContextProtocol.AspNetCore; /// /// Service that handles route-aware tool filtering and automatic route discovery. diff --git a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs index d0c4db06..7ce11306 100644 --- a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs +++ b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs @@ -146,7 +146,7 @@ options.OpenWorld is not null || } } - return new AIFunctionMcpServerTool(function, tool, options?.Services, structuredOutputRequiresWrapping, options?.GetRoutes()); + return new AIFunctionMcpServerTool(function, tool, options?.Services, structuredOutputRequiresWrapping, options?.Routes); } private static McpServerToolCreateOptions DeriveOptions(MethodInfo method, McpServerToolCreateOptions? options) @@ -188,7 +188,7 @@ private static McpServerToolCreateOptions DeriveOptions(MethodInfo method, McpSe if (method.GetCustomAttribute() is { } routeAttr) { - newOptions.SetRoutes(routeAttr.Routes); + newOptions.Routes ??= routeAttr.Routes; } return newOptions; diff --git a/src/ModelContextProtocol.Core/Server/McpServerToolCreateOptions.cs b/src/ModelContextProtocol.Core/Server/McpServerToolCreateOptions.cs index 15b8ffb0..86ea9988 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerToolCreateOptions.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerToolCreateOptions.cs @@ -64,37 +64,21 @@ public sealed class McpServerToolCreateOptions /// public string? Title { get; set; } - private IReadOnlyList? routes; - /// - /// Gets the HTTP routes where this tool should be available. + /// Gets or sets the HTTP routes where this tool should be available. /// - /// - /// An array of route names, or if the tool should be available on all routes (global tool). - /// /// + /// /// Routes are used in AspNetCore scenarios to control which HTTP endpoints expose this tool. + /// If , the tool will be available on all routes (global tool). /// This setting is ignored in stdio/console scenarios. - /// - public IReadOnlyList? GetRoutes() - { - return routes; - } - - /// - /// Sets the HTTP routes where this tool should be available. - /// - /// - /// An array of route names, or to make the tool available on all routes (global tool). - /// - /// + /// + /// /// Routes are typically populated from during tool creation, /// but can be set programmatically when creating tools without attributes. + /// /// - public void SetRoutes(IReadOnlyList? value) - { - routes = value; - } + public IReadOnlyList? Routes { get; set; } /// /// Gets or sets whether the tool may perform destructive updates to its environment. @@ -204,6 +188,6 @@ internal McpServerToolCreateOptions Clone() => UseStructuredContent = UseStructuredContent, SerializerOptions = SerializerOptions, SchemaCreateOptions = SchemaCreateOptions, - routes = routes + Routes = Routes }; }