diff --git a/samples/AspNetCoreMcpServerPerUserTools/AspNetCoreMcpServerPerUserTools.csproj b/samples/AspNetCoreMcpPerSessionTools/AspNetCoreMcpPerSessionTools.csproj similarity index 100% rename from samples/AspNetCoreMcpServerPerUserTools/AspNetCoreMcpServerPerUserTools.csproj rename to samples/AspNetCoreMcpPerSessionTools/AspNetCoreMcpPerSessionTools.csproj diff --git a/samples/AspNetCoreMcpPerSessionTools/Program.cs b/samples/AspNetCoreMcpPerSessionTools/Program.cs new file mode 100644 index 00000000..a0f08b35 --- /dev/null +++ b/samples/AspNetCoreMcpPerSessionTools/Program.cs @@ -0,0 +1,179 @@ +using OpenTelemetry; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; +using AspNetCoreMcpPerSessionTools.Tools; +using ModelContextProtocol.Server; + +var builder = WebApplication.CreateBuilder(args); + +// 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); + var sessionInfo = GetSessionInfo(httpContext); + + // Get the tool collection that we can modify per session + var toolCollection = mcpOptions.Capabilities?.Tools?.ToolCollection; + if (toolCollection != null) + { + // Clear all tools first + toolCollection.Clear(); + + // Add tools based on the requested category + switch (toolCategory?.ToLower()) + { + case "clock": + // Clock category gets time/date tools + AddToolsForType(toolCollection); + break; + + case "calculator": + // Calculator category gets mathematical tools + AddToolsForType(toolCollection); + break; + + case "userinfo": + // UserInfo category gets session and system information tools + AddToolsForType(toolCollection); + break; + + case "all": + default: + // Default or "all" category gets all tools + AddToolsForType(toolCollection); + AddToolsForType(toolCollection); + AddToolsForType(toolCollection); + break; + } + } + + // Optional: Log the session configuration for debugging + var logger = httpContext.RequestServices.GetRequiredService>(); + logger.LogInformation("Configured MCP session for category '{ToolCategory}' from {SessionInfo}, {ToolCount} tools available", + toolCategory, sessionInfo, toolCollection?.Count ?? 0); + }; + }) + .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(); + +// Add middleware to log requests for demo purposes +app.Use(async (context, next) => +{ + var logger = context.RequestServices.GetRequiredService>(); + var toolCategory = GetToolCategoryFromRoute(context); + var sessionInfo = GetSessionInfo(context); + + logger.LogInformation("Request for category '{ToolCategory}' from {SessionInfo}: {Method} {Path}", + toolCategory, sessionInfo, context.Request.Method, context.Request.Path); + + await next(); +}); + +// Map MCP with route parameter for tool category filtering +app.MapMcp("/{toolCategory?}"); + +// Add endpoints to test different tool categories +app.MapGet("/", () => Results.Text( + "MCP Per-Session Tools Demo\n" + + "=========================\n" + + "Available endpoints:\n" + + "- /clock - MCP server with clock/time tools\n" + + "- /calculator - MCP server with calculation tools\n" + + "- /userinfo - MCP server with session/system info tools\n" + + "- /all - MCP server with all tools (default)\n" + + "\n" + + "Test routes:\n" + + "- /test-category/{category} - Test category detection\n" +)); + +app.MapGet("/test-category/{toolCategory?}", (string? toolCategory, HttpContext context) => +{ + var detectedCategory = GetToolCategoryFromRoute(context); + var sessionInfo = GetSessionInfo(context); + + return Results.Text($"Tool Category: {detectedCategory ?? "all (default)"}\n" + + $"Session Info: {sessionInfo}\n" + + $"Route Parameter: {toolCategory ?? "none"}\n" + + $"Message: MCP session would be configured for '{detectedCategory ?? "all"}' tools"); +}); + +app.Run(); + +// Helper methods 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; + } + + // Fallback: try to extract from path + var path = context.Request.Path.Value?.Trim('/'); + if (!string.IsNullOrEmpty(path)) + { + var segments = path.Split('/'); + if (segments.Length > 0) + { + var firstSegment = segments[0].ToLower(); + if (firstSegment is "clock" or "calculator" or "userinfo" or "all") + { + return firstSegment; + } + } + } + + // Default to "all" if no category specified + return "all"; +} + +static string GetSessionInfo(HttpContext context) +{ + var userAgent = context.Request.Headers.UserAgent.ToString(); + var clientInfo = !string.IsNullOrEmpty(userAgent) ? userAgent[..Math.Min(userAgent.Length, 20)] + "..." : "Unknown"; + var remoteIp = context.Connection.RemoteIpAddress?.ToString() ?? "unknown"; + + return $"{clientInfo} ({remoteIp})"; +} + +static void AddToolsForType<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers( + System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicMethods)]T>( + McpServerPrimitiveCollection toolCollection) +{ + var toolType = typeof(T); + var methods = toolType.GetMethods(System.Reflection.BindingFlags.Public | System.Reflection.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()); + toolCollection.Add(tool); + } + catch (Exception ex) + { + // Log error but continue with other tools + Console.WriteLine($"Failed to add tool {toolType.Name}.{method.Name}: {ex.Message}"); + } + } +} \ No newline at end of file diff --git a/samples/AspNetCoreMcpServerPerUserTools/Properties/launchSettings.json b/samples/AspNetCoreMcpPerSessionTools/Properties/launchSettings.json similarity index 100% rename from samples/AspNetCoreMcpServerPerUserTools/Properties/launchSettings.json rename to samples/AspNetCoreMcpPerSessionTools/Properties/launchSettings.json diff --git a/samples/AspNetCoreMcpPerSessionTools/README.md b/samples/AspNetCoreMcpPerSessionTools/README.md new file mode 100644 index 00000000..677f109c --- /dev/null +++ b/samples/AspNetCoreMcpPerSessionTools/README.md @@ -0,0 +1,184 @@ +# 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 MCP SDK's `ConfigureSessionOptions` callback. Instead of using authentication headers, this approach uses URL routes to determine which tools are available to each MCP session, making it easy to test different tool configurations. + +## Features + +- **Route-Based Tool Filtering**: Different routes expose different tool sets +- **Three Tool Categories**: + - **Clock**: Time and date related tools (`/clock`) + - **Calculator**: Mathematical calculation tools (`/calculator`) + - **UserInfo**: Session and system information tools (`/userinfo`) +- **Dynamic Tool Loading**: Tools are filtered per session based on the route used to connect +- **Easy Testing**: Simple URL-based testing without complex authentication setup +- **Comprehensive Logging**: Logs session configuration and tool access for monitoring + +## Tool Categories + +### Clock Tools (`/clock`) +- **GetTime**: Gets the current server time +- **GetDate**: Gets the current date in various formats +- **ConvertTimeZone**: Converts time between timezones (simulated) + +### Calculator Tools (`/calculator`) +- **Calculate**: Performs basic arithmetic operations (+, -, *, /) +- **CalculatePercentage**: Calculates percentage of a number +- **SquareRoot**: Calculates square root of a number + +### UserInfo Tools (`/userinfo`) +- **GetSessionInfo**: Gets information about the current MCP session +- **GetSystemInfo**: Gets system information about the server +- **EchoWithContext**: Echoes messages with session context +- **GetConnectionInfo**: Gets basic connection information + +## 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: GetSessionInfo, GetSystemInfo, EchoWithContext, GetConnectionInfo + +### Testing All Tools +Connect your MCP client to: `https://localhost:5001/all` or `https://localhost:5001/` +- Available tools: All tools from all categories + +### Browser Testing +You can also test the route detection in a browser: +- `https://localhost:5001/` - Shows available endpoints +- `https://localhost:5001/test-category/clock` - Tests clock category detection +- `https://localhost:5001/test-category/calculator` - Tests calculator category detection +- `https://localhost:5001/test-category/userinfo` - Tests userinfo category detection + +## 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 +} +``` + +### 4. Dynamic Tool Loading +The `AddToolsForType` helper method uses reflection to discover and add all tools from a specific tool type to the session's tool collection. + +## Key Benefits + +- **Easy Testing**: No need to manage authentication tokens or headers +- **Clear Separation**: Each tool category is isolated and can be tested independently +- **Flexible Architecture**: Easy to add new tool categories or modify existing ones +- **Production Ready**: The same technique can be extended for production scenarios with proper routing logic +- **Observable**: Built-in logging shows exactly which tools are configured for each session + +## Adapting for Production + +For production use, you might want to: + +1. **Add Authentication**: Combine route-based filtering with proper authentication +2. **Database-Driven Categories**: Load tool categories and permissions from a database +3. **User-Specific Routing**: Use user information to determine allowed categories +4. **Advanced Routing**: Support nested categories or query parameters +5. **Rate Limiting**: Add rate limiting per tool category +6. **Caching**: Cache tool collections for better performance + +## Related Issues + +- [#714](https://github.com/modelcontextprotocol/csharp-sdk/issues/714) - Support varying tools/resources per user +- [#237](https://github.com/modelcontextprotocol/csharp-sdk/issues/237) - Session-specific tool configuration +- [#476](https://github.com/modelcontextprotocol/csharp-sdk/issues/476) - Dynamic tool management +- [#612](https://github.com/modelcontextprotocol/csharp-sdk/issues/612) - Per-session resource filtering + +## Learn More + +- [Model Context Protocol Specification](https://modelcontextprotocol.io/) +- [ASP.NET Core MCP Integration](../../src/ModelContextProtocol.AspNetCore/README.md) +- [MCP C# SDK Documentation](https://modelcontextprotocol.github.io/csharp-sdk/) \ No newline at end of file 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..96d60aa8 --- /dev/null +++ b/samples/AspNetCoreMcpPerSessionTools/Tools/UserInfoTool.cs @@ -0,0 +1,50 @@ +using ModelContextProtocol.Server; +using System.ComponentModel; + +namespace AspNetCoreMcpPerSessionTools.Tools; + +/// +/// User information and session-related tools +/// +[McpServerToolType] +public sealed class UserInfoTool +{ + [McpServerTool, Description("Gets information about the current MCP session.")] + public static string GetSessionInfo() + { + return $"MCP Session Information:\n" + + $"- Session ID: {Guid.NewGuid():N}[..8] (simulated)\n" + + $"- Session Start: {DateTime.Now.AddMinutes(-new Random().Next(1, 60)):HH:mm:ss}\n" + + $"- Protocol Version: MCP 2025-06-18\n" + + $"- Transport: HTTP"; + } + + [McpServerTool, Description("Gets system information about the server environment.")] + public static string GetSystemInfo() + { + return $"System Information:\n" + + $"- Server Time: {DateTime.Now:yyyy-MM-dd HH:mm:ss} UTC\n" + + $"- Platform: {Environment.OSVersion.Platform}\n" + + $"- Runtime: .NET {Environment.Version}\n" + + $"- Processor Count: {Environment.ProcessorCount}\n" + + $"- Working Set: {Environment.WorkingSet / (1024 * 1024):F1} MB"; + } + + [McpServerTool, Description("Echoes back the provided message with session context.")] + public static string EchoWithContext( + [Description("Message to echo back")] string message) + { + return $"[Session Echo] {DateTime.Now:HH:mm:ss}: {message}"; + } + + [Description("Gets basic connection information about the client.")] + [McpServerTool] + public static string GetConnectionInfo() + { + return $"Connection Information:\n" + + $"- Connection Type: HTTP MCP Transport\n" + + $"- Connected At: {DateTime.Now.AddMinutes(-new Random().Next(1, 30)):HH:mm:ss}\n" + + $"- Status: Active\n" + + $"- Messages Exchanged: {new Random().Next(1, 100)} (simulated)"; + } +} \ No newline at end of file diff --git a/samples/AspNetCoreMcpServerPerUserTools/appsettings.Development.json b/samples/AspNetCoreMcpPerSessionTools/appsettings.Development.json similarity index 100% rename from samples/AspNetCoreMcpServerPerUserTools/appsettings.Development.json rename to samples/AspNetCoreMcpPerSessionTools/appsettings.Development.json diff --git a/samples/AspNetCoreMcpServerPerUserTools/appsettings.json b/samples/AspNetCoreMcpPerSessionTools/appsettings.json similarity index 74% rename from samples/AspNetCoreMcpServerPerUserTools/appsettings.json rename to samples/AspNetCoreMcpPerSessionTools/appsettings.json index a7f1eede..88c89fa7 100644 --- a/samples/AspNetCoreMcpServerPerUserTools/appsettings.json +++ b/samples/AspNetCoreMcpPerSessionTools/appsettings.json @@ -3,7 +3,7 @@ "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning", - "AspNetCoreMcpServerPerUserTools": "Debug" + "AspNetCoreMcpPerSessionTools": "Debug" } }, "AllowedHosts": "*" diff --git a/samples/AspNetCoreMcpServerPerUserTools/Program.cs b/samples/AspNetCoreMcpServerPerUserTools/Program.cs deleted file mode 100644 index 7606d2e1..00000000 --- a/samples/AspNetCoreMcpServerPerUserTools/Program.cs +++ /dev/null @@ -1,172 +0,0 @@ -using OpenTelemetry; -using OpenTelemetry.Metrics; -using OpenTelemetry.Trace; -using AspNetCoreMcpServerPerUserTools.Tools; -using ModelContextProtocol.Server; - -var builder = WebApplication.CreateBuilder(args); - -// Register all MCP server tools - they will be filtered per user later -builder.Services.AddMcpServer() - .WithHttpTransport(options => - { - // Configure per-session options to filter tools based on user permissions - options.ConfigureSessionOptions = async (httpContext, mcpOptions, cancellationToken) => - { - // Determine user role from headers (in real apps, use proper authentication) - var userRole = GetUserRole(httpContext); - var userId = GetUserId(httpContext); - - // Get the tool collection that we can modify per session - var toolCollection = mcpOptions.Capabilities?.Tools?.ToolCollection; - if (toolCollection != null) - { - // Clear all tools first - toolCollection.Clear(); - - // Add tools based on user role - switch (userRole) - { - case "admin": - // Admins get all tools - AddToolsForType(toolCollection); - AddToolsForType(toolCollection); - AddToolsForType(toolCollection); - break; - - case "user": - // Regular users get public and user tools - AddToolsForType(toolCollection); - AddToolsForType(toolCollection); - break; - - default: - // Anonymous/public users get only public tools - AddToolsForType(toolCollection); - break; - } - } - - // Optional: Log the session configuration for debugging - var logger = httpContext.RequestServices.GetRequiredService>(); - logger.LogInformation("Configured MCP session for user {UserId} with role {UserRole}, {ToolCount} tools available", - userId, userRole, toolCollection?.Count ?? 0); - }; - }) - .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(); - -// Add middleware to log requests for demo purposes -app.Use(async (context, next) => -{ - var logger = context.RequestServices.GetRequiredService>(); - var userRole = GetUserRole(context); - var userId = GetUserId(context); - - logger.LogInformation("Request from User {UserId} with Role {UserRole}: {Method} {Path}", - userId, userRole, context.Request.Method, context.Request.Path); - - await next(); -}); - -app.MapMcp(); - -// Add a simple endpoint to test authentication headers -app.MapGet("/test-auth", (HttpContext context) => -{ - var userRole = GetUserRole(context); - var userId = GetUserId(context); - - return Results.Text($"UserId: {userId}\nRole: {userRole}\nMessage: You are authenticated as {userId} with role {userRole}"); -}); - -app.Run(); - -// Helper methods for authentication - in production, use proper authentication/authorization -static string GetUserRole(HttpContext context) -{ - // Check for X-User-Role header first - if (context.Request.Headers.TryGetValue("X-User-Role", out var roleHeader)) - { - var role = roleHeader.ToString().ToLowerInvariant(); - if (role is "admin" or "user" or "public") - { - return role; - } - } - - // Check for Authorization header pattern (Bearer token simulation) - if (context.Request.Headers.TryGetValue("Authorization", out var authHeader)) - { - var auth = authHeader.ToString(); - if (auth.StartsWith("Bearer admin-", StringComparison.OrdinalIgnoreCase)) - return "admin"; - if (auth.StartsWith("Bearer user-", StringComparison.OrdinalIgnoreCase)) - return "user"; - if (auth.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) - return "public"; - } - - // Default to public access - return "public"; -} - -static string GetUserId(HttpContext context) -{ - // Check for X-User-Id header first - if (context.Request.Headers.TryGetValue("X-User-Id", out var userIdHeader)) - { - return userIdHeader.ToString(); - } - - // Extract from Authorization header if present - if (context.Request.Headers.TryGetValue("Authorization", out var authHeader)) - { - var auth = authHeader.ToString(); - if (auth.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) - { - var token = auth["Bearer ".Length..]; - return token.Contains('-') ? token : $"user-{token}"; - } - } - - // Generate anonymous ID - return $"anonymous-{Guid.NewGuid():N}"[..16]; -} - -static void AddToolsForType<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers( - System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicMethods)]T>( - McpServerPrimitiveCollection toolCollection) -{ - var toolType = typeof(T); - var methods = toolType.GetMethods(System.Reflection.BindingFlags.Public | System.Reflection.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()); - toolCollection.Add(tool); - } - catch (Exception ex) - { - // Log error but continue with other tools - Console.WriteLine($"Failed to add tool {toolType.Name}.{method.Name}: {ex.Message}"); - } - } -} \ No newline at end of file diff --git a/samples/AspNetCoreMcpServerPerUserTools/README.md b/samples/AspNetCoreMcpServerPerUserTools/README.md deleted file mode 100644 index 6ac73d23..00000000 --- a/samples/AspNetCoreMcpServerPerUserTools/README.md +++ /dev/null @@ -1,234 +0,0 @@ -# ASP.NET Core MCP Server with Per-User Tool Filtering - -This sample demonstrates how to create an MCP (Model Context Protocol) server that provides different sets of tools to different users based on their authentication and permissions. This addresses the requirement from [issue #714](https://github.com/modelcontextprotocol/csharp-sdk/issues/714) to support varying the list of available tools/resources per user. - -## Overview - -The sample showcases the technique described by @halter73 in issue #714, using the `ConfigureSessionOptions` callback to dynamically modify the `ToolCollection` based on user permissions for each MCP session. - -## Features - -- **Per-User Tool Filtering**: Different users see different tools based on their role -- **Three Permission Levels**: - - **Public**: Basic tools available to all users (echo, time) - - **User**: Additional tools for authenticated users (user info, calculator) - - **Admin**: Full access including system administration tools -- **Header-based Authentication**: Simple authentication mechanism using HTTP headers -- **Dynamic Tool Loading**: Tools are filtered per session, not globally -- **Audit Logging**: Logs user sessions and tool access for monitoring - -## Tool Categories - -### Public Tools (`PublicTool.cs`) -Available to all users without authentication: -- `echo` - Echo messages back to the client -- `get_time` - Get current server time - -### User Tools (`UserTool.cs`) -Available to authenticated users: -- `get_user_info` - Get personalized user information -- `calculate` - Perform basic mathematical calculations - -### Admin Tools (`AdminTool.cs`) -Available only to administrators: -- `get_system_status` - View system status and performance metrics -- `manage_config` - Manage server configuration settings -- `view_audit_logs` - View audit logs and user activity - -## Authentication - -The sample uses a simple header-based authentication system suitable for development and testing. In production, replace this with proper authentication/authorization (e.g., JWT, OAuth, ASP.NET Core Identity). - -### Authentication Headers - -#### Option 1: Role-based Headers -```bash -# Admin user -X-User-Id: admin-john -X-User-Role: admin - -# Regular user -X-User-Id: user-alice -X-User-Role: user - -# Public user -X-User-Id: public-bob -X-User-Role: public -``` - -#### Option 2: Bearer Token Pattern -```bash -# Admin user -Authorization: Bearer admin-token123 - -# Regular user -Authorization: Bearer user-token456 - -# Public user -Authorization: Bearer token789 -``` - -## Running the Sample - -1. **Build and run the server:** - ```bash - cd samples/AspNetCoreMcpServerPerUserTools - dotnet run - ``` - -2. **Test authentication endpoint:** - ```bash - # Test admin user - curl -H "X-User-Role: admin" -H "X-User-Id: admin-john" \ - http://localhost:3001/test-auth - - # Test regular user - curl -H "X-User-Role: user" -H "X-User-Id: user-alice" \ - http://localhost:3001/test-auth - ``` - -3. **Connect MCP client and test tool filtering:** - - The MCP server will be available at `http://localhost:3001/` for MCP protocol connections. - -## Testing Tool Access - -### As Anonymous User (Public Tools Only) -```bash -# Will see only: echo, get_time -curl -X POST http://localhost:3001/ \ - -H "Content-Type: application/json" \ - -H "Accept: application/json, text/event-stream" \ - -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' -``` - -### As Regular User -```bash -# Will see: echo, get_time, get_user_info, calculate -curl -X POST http://localhost:3001/ \ - -H "Content-Type: application/json" \ - -H "Accept: application/json, text/event-stream" \ - -H "X-User-Role: user" \ - -H "X-User-Id: user-alice" \ - -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' -``` - -### As Admin User -```bash -# Will see all tools: echo, get_time, get_user_info, calculate, get_system_status, manage_config, view_audit_logs -curl -X POST http://localhost:3001/ \ - -H "Content-Type: application/json" \ - -H "Accept: application/json, text/event-stream" \ - -H "X-User-Role: admin" \ - -H "X-User-Id: admin-john" \ - -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' -``` - -## 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. Per-Session Filtering -The key technique is using `ConfigureSessionOptions` to modify the tool collection per session: - -```csharp -.WithHttpTransport(options => -{ - options.ConfigureSessionOptions = async (httpContext, mcpOptions, cancellationToken) => - { - var userRole = GetUserRole(httpContext); - var toolCollection = mcpOptions.Capabilities?.Tools?.ToolCollection; - - if (toolCollection != null) - { - // Clear all tools and add back only those allowed for this user - toolCollection.Clear(); - - switch (userRole) - { - case "admin": - AddToolsForType(toolCollection); - AddToolsForType(toolCollection); - AddToolsForType(toolCollection); - break; - case "user": - AddToolsForType(toolCollection); - AddToolsForType(toolCollection); - break; - default: - AddToolsForType(toolCollection); - break; - } - } - }; -}) -``` - -### 3. Authentication Logic -Simple authentication extracts user information from HTTP headers: - -```csharp -static string GetUserRole(HttpContext context) -{ - // Check X-User-Role header or Authorization pattern - // Returns "admin", "user", or "public" -} - -static string GetUserId(HttpContext context) -{ - // Extract user ID from headers - // Returns user identifier -} -``` - -### 4. Dynamic Tool Loading -A helper method recreates tool instances for the filtered collection: - -```csharp -static void AddToolsForType(McpServerPrimitiveCollection toolCollection) -{ - // Use reflection to find and recreate tools from the specified type - // Add them to the session-specific tool collection -} -``` - -## Key Benefits - -1. **Security**: Users only see tools they're authorized to use -2. **Scalability**: Per-session filtering doesn't affect other users -3. **Flexibility**: Easy to add new roles and permission levels -4. **Maintainability**: Clear separation between authentication and tool logic -5. **Performance**: Tools are filtered at session start, not per request - -## Adapting for Production - -To use this pattern in production: - -1. **Replace header-based auth** with proper authentication (JWT, OAuth2, etc.) -2. **Add authorization policies** using ASP.NET Core's authorization framework -3. **Store permissions in database** instead of hardcoded role checks -4. **Add caching** for permission lookups to improve performance -5. **Implement proper logging** and monitoring for security events -6. **Add rate limiting** and other security measures - -## Related Issues - -- [#714](https://github.com/modelcontextprotocol/csharp-sdk/issues/714) - Support varying tools/resources per user -- [#222](https://github.com/modelcontextprotocol/csharp-sdk/issues/222) - Related per-user filtering discussion -- [#237](https://github.com/modelcontextprotocol/csharp-sdk/issues/237) - Session-specific tool configuration -- [#476](https://github.com/modelcontextprotocol/csharp-sdk/issues/476) - Dynamic tool management -- [#612](https://github.com/modelcontextprotocol/csharp-sdk/issues/612) - Per-session resource filtering - -## Learn More - -- [Model Context Protocol Specification](https://modelcontextprotocol.io/) -- [ASP.NET Core MCP Integration](../../src/ModelContextProtocol.AspNetCore/README.md) -- [MCP C# SDK Documentation](https://modelcontextprotocol.github.io/csharp-sdk/) \ No newline at end of file diff --git a/samples/AspNetCoreMcpServerPerUserTools/Tools/AdminTool.cs b/samples/AspNetCoreMcpServerPerUserTools/Tools/AdminTool.cs deleted file mode 100644 index 19c62f06..00000000 --- a/samples/AspNetCoreMcpServerPerUserTools/Tools/AdminTool.cs +++ /dev/null @@ -1,51 +0,0 @@ -using ModelContextProtocol.Server; -using System.ComponentModel; - -namespace AspNetCoreMcpServerPerUserTools.Tools; - -/// -/// Administrative tools available only to admin users -/// -[McpServerToolType] -public sealed class AdminTool -{ - [McpServerTool, Description("Gets system status information. Requires admin privileges.")] - public static string GetSystemStatus() - { - var uptime = Environment.TickCount64; - var memoryUsage = GC.GetTotalMemory(false); - - return $"System Status:\n" + - $"- Uptime: {TimeSpan.FromMilliseconds(uptime):dd\\.hh\\:mm\\:ss}\n" + - $"- Memory Usage: {memoryUsage / (1024 * 1024):F2} MB\n" + - $"- Processor Count: {Environment.ProcessorCount}\n" + - $"- OS Version: {Environment.OSVersion}"; - } - - [McpServerTool, Description("Manages server configuration. Requires admin privileges.")] - public static string ManageConfig( - [Description("Configuration action to perform")] string action, - [Description("Configuration key")] string? key = null, - [Description("Configuration value")] string? value = null) - { - return action.ToLower() switch - { - "list" => "Available configs:\n- debug_mode: false\n- max_connections: 100\n- log_level: info", - "get" when !string.IsNullOrEmpty(key) => $"Config '{key}': [simulated value for {key}]", - "set" when !string.IsNullOrEmpty(key) && !string.IsNullOrEmpty(value) => - $"Config '{key}' set to '{value}' (simulated)", - _ => "Usage: action must be 'list', 'get' (with key), or 'set' (with key and value)" - }; - } - - [McpServerTool, Description("Views audit logs. Requires admin privileges.")] - public static string ViewAuditLogs([Description("Number of recent entries to show")] int count = 10) - { - var logs = new List(); - for (int i = 0; i < Math.Min(count, 10); i++) - { - logs.Add($"{DateTime.Now.AddMinutes(-i):HH:mm:ss} - User action logged (simulated entry {i + 1})"); - } - return $"Recent audit logs ({logs.Count} entries):\n" + string.Join("\n", logs); - } -} \ No newline at end of file diff --git a/samples/AspNetCoreMcpServerPerUserTools/Tools/PublicTool.cs b/samples/AspNetCoreMcpServerPerUserTools/Tools/PublicTool.cs deleted file mode 100644 index a5d351df..00000000 --- a/samples/AspNetCoreMcpServerPerUserTools/Tools/PublicTool.cs +++ /dev/null @@ -1,23 +0,0 @@ -using ModelContextProtocol.Server; -using System.ComponentModel; - -namespace AspNetCoreMcpServerPerUserTools.Tools; - -/// -/// Public tools available to all users (no permission required) -/// -[McpServerToolType] -public sealed class PublicTool -{ - [McpServerTool, Description("Echoes the input back to the client. Available to all users.")] - public static string Echo(string message) - { - return "Echo: " + message; - } - - [McpServerTool, Description("Gets the current server time. Available to all users.")] - public static string GetTime() - { - return $"Current server time: {DateTime.Now:yyyy-MM-dd HH:mm:ss} UTC"; - } -} \ No newline at end of file diff --git a/samples/AspNetCoreMcpServerPerUserTools/Tools/UserTool.cs b/samples/AspNetCoreMcpServerPerUserTools/Tools/UserTool.cs deleted file mode 100644 index 8f2c1b64..00000000 --- a/samples/AspNetCoreMcpServerPerUserTools/Tools/UserTool.cs +++ /dev/null @@ -1,48 +0,0 @@ -using ModelContextProtocol.Server; -using System.ComponentModel; - -namespace AspNetCoreMcpServerPerUserTools.Tools; - -/// -/// User-level tools available to authenticated users -/// -[McpServerToolType] -public sealed class UserTool -{ - [McpServerTool, Description("Gets personalized user information. Requires user authentication.")] - public static string GetUserInfo(string? userId = null) - { - return $"User information for: {userId ?? "current user"}. Profile: Standard User"; - } - - [McpServerTool, Description("Performs basic calculations. Available to authenticated users.")] - public static string Calculate([Description("Mathematical expression to evaluate")] string expression) - { - // Simple calculator for demo purposes - try - { - // For demo, just handle basic addition/subtraction - 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}"; - } - } - return $"Cannot evaluate expression: {expression}. Try simple addition (a + b) or subtraction (a - b)."; - } - catch - { - return $"Error evaluating: {expression}"; - } - } -} \ No newline at end of file