diff --git a/samples/AspNetCoreMcpPerSessionTools/AspNetCoreMcpPerSessionTools.csproj b/samples/AspNetCoreMcpPerSessionTools/AspNetCoreMcpPerSessionTools.csproj new file mode 100644 index 00000000..567374bc --- /dev/null +++ b/samples/AspNetCoreMcpPerSessionTools/AspNetCoreMcpPerSessionTools.csproj @@ -0,0 +1,21 @@ + + + + net9.0 + enable + enable + true + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/AspNetCoreMcpPerSessionTools/Program.cs b/samples/AspNetCoreMcpPerSessionTools/Program.cs new file mode 100644 index 00000000..098a363c --- /dev/null +++ b/samples/AspNetCoreMcpPerSessionTools/Program.cs @@ -0,0 +1,122 @@ +using OpenTelemetry; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; +using AspNetCoreMcpPerSessionTools.Tools; +using ModelContextProtocol.Server; +using System.Collections.Concurrent; +using System.Reflection; + +var builder = WebApplication.CreateBuilder(args); + +// Create and populate the tool dictionary at startup +var toolDictionary = new ConcurrentDictionary(); +PopulateToolDictionary(toolDictionary); + +// Register all MCP server tools - they will be filtered per session based on route +builder.Services.AddMcpServer() + .WithHttpTransport(options => + { + // Configure per-session options to filter tools based on route category + options.ConfigureSessionOptions = async (httpContext, mcpOptions, cancellationToken) => + { + // Determine tool category from route parameters + var toolCategory = GetToolCategoryFromRoute(httpContext); + + // Get the tool collection that we can modify per session + var toolCollection = mcpOptions.Capabilities?.Tools?.ToolCollection; + if (toolCollection == null) + { + return; + } + + // Clear tools (we add them to make sure the capability is initialized) + toolCollection.Clear(); + + // Get pre-populated tools for the requested category + if (toolDictionary.TryGetValue(toolCategory.ToLower(), out var tools)) + { + foreach (var tool in tools) + { + toolCollection.Add(tool); + } + } + }; + }) + .WithTools() + .WithTools() + .WithTools(); + +// Add OpenTelemetry for observability +builder.Services.AddOpenTelemetry() + .WithTracing(b => b.AddSource("*") + .AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation()) + .WithMetrics(b => b.AddMeter("*") + .AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation()) + .WithLogging() + .UseOtlpExporter(); + +var app = builder.Build(); + +// Map MCP with route parameter for tool category filtering +app.MapMcp("/{toolCategory?}"); + +app.Run(); + +// Helper method for route-based tool category detection +static string GetToolCategoryFromRoute(HttpContext context) +{ + // Try to get tool category from route values + if (context.Request.RouteValues.TryGetValue("toolCategory", out var categoryObj) && categoryObj is string category) + { + return string.IsNullOrEmpty(category) ? "all" : category; + } + + // Default to "all" if no category specified or empty + return "all"; +} + +// Helper method to populate the tool dictionary at startup +static void PopulateToolDictionary(ConcurrentDictionary toolDictionary) +{ + // Get tools for each category + var clockTools = GetToolsForType(); + var calculatorTools = GetToolsForType(); + var userInfoTools = GetToolsForType(); + McpServerTool[] allTools = [.. clockTools, + .. calculatorTools, + .. userInfoTools]; + + // Populate the dictionary with tools for each category + toolDictionary.TryAdd("clock", clockTools); + toolDictionary.TryAdd("calculator", calculatorTools); + toolDictionary.TryAdd("userinfo", userInfoTools); + toolDictionary.TryAdd("all", allTools); +} + +// Helper method to get tools for a specific type using reflection +static McpServerTool[] GetToolsForType<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers( + System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicMethods)] T>() +{ + var tools = new List(); + var toolType = typeof(T); + var methods = toolType.GetMethods(BindingFlags.Public | BindingFlags.Static) + .Where(m => m.GetCustomAttributes(typeof(McpServerToolAttribute), false).Any()); + + foreach (var method in methods) + { + try + { + var tool = McpServerTool.Create(method, target: null, new McpServerToolCreateOptions()); + tools.Add(tool); + } + catch (Exception ex) + { + // Log error but continue with other tools + Console.WriteLine($"Failed to add tool {toolType.Name}.{method.Name}: {ex.Message}"); + } + } + + return [.. tools]; +} \ No newline at end of file diff --git a/samples/AspNetCoreMcpPerSessionTools/Properties/launchSettings.json b/samples/AspNetCoreMcpPerSessionTools/Properties/launchSettings.json new file mode 100644 index 00000000..da8208a1 --- /dev/null +++ b/samples/AspNetCoreMcpPerSessionTools/Properties/launchSettings.json @@ -0,0 +1,13 @@ +{ + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:3001", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} \ No newline at end of file diff --git a/samples/AspNetCoreMcpPerSessionTools/README.md b/samples/AspNetCoreMcpPerSessionTools/README.md new file mode 100644 index 00000000..8e366510 --- /dev/null +++ b/samples/AspNetCoreMcpPerSessionTools/README.md @@ -0,0 +1,113 @@ +# ASP.NET Core MCP Server with Per-Session Tool Filtering + +This sample demonstrates how to create an MCP (Model Context Protocol) server that provides different sets of tools based on route-based session configuration. This showcases the technique of using `ConfigureSessionOptions` to dynamically modify the `ToolCollection` based on route parameters for each MCP session. + +## Overview + +The sample demonstrates route-based tool filtering using the SDK's `ConfigureSessionOptions` callback. You could use any mechanism, routing is just one way to achieve this. The point of the sample is to show how an MCP server can dynamically adjust the available tools for each session based on arbitrary criteria, in this case, the URL route. + +## Route-Based Configuration + +The server uses route parameters to determine which tools to make available: + +- `GET /clock` - MCP server with only clock/time tools +- `GET /calculator` - MCP server with only calculation tools +- `GET /userinfo` - MCP server with only session/system info tools +- `GET /all` or `GET /` - MCP server with all tools (default) + +## Running the Sample + +1. Navigate to the sample directory: + ```bash + cd samples/AspNetCoreMcpPerSessionTools + ``` + +2. Run the server: + ```bash + dotnet run + ``` + +3. The server will start on `https://localhost:5001` (or the port shown in the console) + +## Testing Tool Categories + +### Testing Clock Tools +Connect your MCP client to: `https://localhost:5001/clock` +- Available tools: GetTime, GetDate, ConvertTimeZone + +### Testing Calculator Tools +Connect your MCP client to: `https://localhost:5001/calculator` +- Available tools: Calculate, CalculatePercentage, SquareRoot + +### Testing UserInfo Tools +Connect your MCP client to: `https://localhost:5001/userinfo` +- Available tools: GetUserInfo + +### Testing All Tools +Connect your MCP client to: `https://localhost:5001/all` or `https://localhost:5001/` +- Available tools: All tools from all categories + +## How It Works + +### 1. Tool Registration +All tools are registered during startup using the normal MCP tool registration: + +```csharp +builder.Services.AddMcpServer() + .WithTools() + .WithTools() + .WithTools(); +``` + +### 2. Route-Based Session Filtering +The key technique is using `ConfigureSessionOptions` to modify the tool collection per session based on the route: + +```csharp +.WithHttpTransport(options => +{ + options.ConfigureSessionOptions = async (httpContext, mcpOptions, cancellationToken) => + { + var toolCategory = GetToolCategoryFromRoute(httpContext); + var toolCollection = mcpOptions.Capabilities?.Tools?.ToolCollection; + + if (toolCollection != null) + { + // Clear all tools and add back only those for this category + toolCollection.Clear(); + + switch (toolCategory?.ToLower()) + { + case "clock": + AddToolsForType(toolCollection); + break; + case "calculator": + AddToolsForType(toolCollection); + break; + case "userinfo": + AddToolsForType(toolCollection); + break; + default: + // All tools for default/all category + AddToolsForType(toolCollection); + AddToolsForType(toolCollection); + AddToolsForType(toolCollection); + break; + } + } + }; +}) +``` + +### 3. Route Parameter Detection +The `GetToolCategoryFromRoute` method extracts the tool category from the URL route: + +```csharp +static string? GetToolCategoryFromRoute(HttpContext context) +{ + if (context.Request.RouteValues.TryGetValue("toolCategory", out var categoryObj) && categoryObj is string category) + { + return string.IsNullOrEmpty(category) ? "all" : category; + } + return "all"; // Default +} +``` diff --git a/samples/AspNetCoreMcpPerSessionTools/Tools/CalculatorTool.cs b/samples/AspNetCoreMcpPerSessionTools/Tools/CalculatorTool.cs new file mode 100644 index 00000000..c6d9f621 --- /dev/null +++ b/samples/AspNetCoreMcpPerSessionTools/Tools/CalculatorTool.cs @@ -0,0 +1,81 @@ +using ModelContextProtocol.Server; +using System.ComponentModel; + +namespace AspNetCoreMcpPerSessionTools.Tools; + +/// +/// Calculator tools for mathematical operations +/// +[McpServerToolType] +public sealed class CalculatorTool +{ + [McpServerTool, Description("Performs basic arithmetic calculations (addition, subtraction, multiplication, division).")] + public static string Calculate([Description("Mathematical expression to evaluate (e.g., '5 + 3', '10 - 2', '4 * 6', '15 / 3')")] string expression) + { + try + { + // Simple calculator for demo purposes - supports basic operations + expression = expression.Trim(); + + if (expression.Contains("+")) + { + var parts = expression.Split('+'); + if (parts.Length == 2 && double.TryParse(parts[0].Trim(), out var a) && double.TryParse(parts[1].Trim(), out var b)) + { + return $"{expression} = {a + b}"; + } + } + else if (expression.Contains("-")) + { + var parts = expression.Split('-'); + if (parts.Length == 2 && double.TryParse(parts[0].Trim(), out var a) && double.TryParse(parts[1].Trim(), out var b)) + { + return $"{expression} = {a - b}"; + } + } + else if (expression.Contains("*")) + { + var parts = expression.Split('*'); + if (parts.Length == 2 && double.TryParse(parts[0].Trim(), out var a) && double.TryParse(parts[1].Trim(), out var b)) + { + return $"{expression} = {a * b}"; + } + } + else if (expression.Contains("/")) + { + var parts = expression.Split('/'); + if (parts.Length == 2 && double.TryParse(parts[0].Trim(), out var a) && double.TryParse(parts[1].Trim(), out var b)) + { + if (b == 0) + return "Error: Division by zero"; + return $"{expression} = {a / b}"; + } + } + + return $"Cannot evaluate expression: {expression}. Supported operations: +, -, *, / (e.g., '5 + 3')"; + } + catch (Exception ex) + { + return $"Error evaluating '{expression}': {ex.Message}"; + } + } + + [McpServerTool, Description("Calculates percentage of a number.")] + public static string CalculatePercentage( + [Description("The number to calculate percentage of")] double number, + [Description("The percentage value")] double percentage) + { + var result = (number * percentage) / 100; + return $"{percentage}% of {number} = {result}"; + } + + [McpServerTool, Description("Calculates the square root of a number.")] + public static string SquareRoot([Description("The number to find square root of")] double number) + { + if (number < 0) + return "Error: Cannot calculate square root of negative number"; + + var result = Math.Sqrt(number); + return $"√{number} = {result}"; + } +} \ No newline at end of file diff --git a/samples/AspNetCoreMcpPerSessionTools/Tools/ClockTool.cs b/samples/AspNetCoreMcpPerSessionTools/Tools/ClockTool.cs new file mode 100644 index 00000000..d112de0f --- /dev/null +++ b/samples/AspNetCoreMcpPerSessionTools/Tools/ClockTool.cs @@ -0,0 +1,40 @@ +using ModelContextProtocol.Server; +using System.ComponentModel; + +namespace AspNetCoreMcpPerSessionTools.Tools; + +/// +/// Clock-related tools for time and date operations +/// +[McpServerToolType] +public sealed class ClockTool +{ + [McpServerTool, Description("Gets the current server time in various formats.")] + public static string GetTime() + { + return $"Current server time: {DateTime.Now:yyyy-MM-dd HH:mm:ss} UTC"; + } + + [McpServerTool, Description("Gets the current date in a specific format.")] + public static string GetDate([Description("Date format (e.g., 'yyyy-MM-dd', 'MM/dd/yyyy')")] string format = "yyyy-MM-dd") + { + try + { + return $"Current date: {DateTime.Now.ToString(format)}"; + } + catch (FormatException) + { + return $"Invalid format '{format}'. Using default: {DateTime.Now:yyyy-MM-dd}"; + } + } + + [McpServerTool, Description("Converts time between timezones.")] + public static string ConvertTimeZone( + [Description("Source timezone (e.g., 'UTC', 'EST')")] string fromTimeZone = "UTC", + [Description("Target timezone (e.g., 'PST', 'GMT')")] string toTimeZone = "PST") + { + // Simplified timezone conversion for demo purposes + var now = DateTime.Now; + return $"Time conversion from {fromTimeZone} to {toTimeZone}: {now:HH:mm:ss} (simulated)"; + } +} \ No newline at end of file diff --git a/samples/AspNetCoreMcpPerSessionTools/Tools/UserInfoTool.cs b/samples/AspNetCoreMcpPerSessionTools/Tools/UserInfoTool.cs new file mode 100644 index 00000000..1fec1873 --- /dev/null +++ b/samples/AspNetCoreMcpPerSessionTools/Tools/UserInfoTool.cs @@ -0,0 +1,23 @@ +using ModelContextProtocol.Server; +using System.ComponentModel; + +namespace AspNetCoreMcpPerSessionTools.Tools; + +/// +/// User information tools +/// +[McpServerToolType] +public sealed class UserInfoTool +{ + [McpServerTool, Description("Gets information about the current user in the MCP session.")] + public static string GetUserInfo() + { + // Dummy user information for demonstration purposes + return $"User Information:\n" + + $"- User ID: {Guid.NewGuid():N}[..8] (simulated)\n" + + $"- Username: User{new Random().Next(1, 1000)}\n" + + $"- Roles: User, Guest\n" + + $"- Last Login: {DateTime.Now.AddMinutes(-new Random().Next(1, 60)):HH:mm:ss}\n" + + $"- Account Status: Active"; + } +} \ No newline at end of file diff --git a/samples/AspNetCoreMcpPerSessionTools/appsettings.Development.json b/samples/AspNetCoreMcpPerSessionTools/appsettings.Development.json new file mode 100644 index 00000000..f999bc20 --- /dev/null +++ b/samples/AspNetCoreMcpPerSessionTools/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + } +} \ No newline at end of file diff --git a/samples/AspNetCoreMcpPerSessionTools/appsettings.json b/samples/AspNetCoreMcpPerSessionTools/appsettings.json new file mode 100644 index 00000000..88c89fa7 --- /dev/null +++ b/samples/AspNetCoreMcpPerSessionTools/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "AspNetCoreMcpPerSessionTools": "Debug" + } + }, + "AllowedHosts": "*" +} \ No newline at end of file