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..e6ff2489
--- /dev/null
+++ b/src/ModelContextProtocol.AspNetCore/McpRouteAwarenessExtension.cs
@@ -0,0 +1,228 @@
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Routing;
+using Microsoft.Extensions.DependencyInjection;
+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/RouteAwareToolService.cs b/src/ModelContextProtocol.AspNetCore/RouteAwareToolService.cs
new file mode 100644
index 00000000..64da5179
--- /dev/null
+++ b/src/ModelContextProtocol.AspNetCore/RouteAwareToolService.cs
@@ -0,0 +1,124 @@
+using Microsoft.Extensions.Options;
+using ModelContextProtocol.Protocol;
+using ModelContextProtocol.Server;
+
+namespace ModelContextProtocol.AspNetCore;
+
+///
+/// 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..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);
+ return new AIFunctionMcpServerTool(function, tool, options?.Services, structuredOutputRequiresWrapping, options?.Routes);
}
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.Routes ??= 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..86ea9988 100644
--- a/src/ModelContextProtocol.Core/Server/McpServerToolCreateOptions.cs
+++ b/src/ModelContextProtocol.Core/Server/McpServerToolCreateOptions.cs
@@ -64,6 +64,22 @@ public sealed class McpServerToolCreateOptions
///
public string? Title { get; set; }
+ ///
+ /// Gets or sets the HTTP routes where this tool should be available.
+ ///
+ ///
+ ///
+ /// 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.
+ ///
+ ///
+ /// Routes are typically populated from during tool creation,
+ /// but can be set programmatically when creating tools without attributes.
+ ///
+ ///
+ public IReadOnlyList? Routes { get; set; }
+
///
/// Gets or sets whether the tool may perform destructive updates to its environment.
///
@@ -172,5 +188,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