diff --git a/samples/AspNetCoreMcpServerPerUserTools/AspNetCoreMcpServerPerUserTools.csproj b/samples/AspNetCoreMcpServerPerUserTools/AspNetCoreMcpServerPerUserTools.csproj
new file mode 100644
index 000000000..567374bcd
--- /dev/null
+++ b/samples/AspNetCoreMcpServerPerUserTools/AspNetCoreMcpServerPerUserTools.csproj
@@ -0,0 +1,21 @@
+
+
+
+ net9.0
+ enable
+ enable
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/AspNetCoreMcpServerPerUserTools/Program.cs b/samples/AspNetCoreMcpServerPerUserTools/Program.cs
new file mode 100644
index 000000000..7606d2e16
--- /dev/null
+++ b/samples/AspNetCoreMcpServerPerUserTools/Program.cs
@@ -0,0 +1,172 @@
+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/Properties/launchSettings.json b/samples/AspNetCoreMcpServerPerUserTools/Properties/launchSettings.json
new file mode 100644
index 000000000..da8208a11
--- /dev/null
+++ b/samples/AspNetCoreMcpServerPerUserTools/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/AspNetCoreMcpServerPerUserTools/README.md b/samples/AspNetCoreMcpServerPerUserTools/README.md
new file mode 100644
index 000000000..6ac73d23e
--- /dev/null
+++ b/samples/AspNetCoreMcpServerPerUserTools/README.md
@@ -0,0 +1,234 @@
+# 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
new file mode 100644
index 000000000..19c62f06e
--- /dev/null
+++ b/samples/AspNetCoreMcpServerPerUserTools/Tools/AdminTool.cs
@@ -0,0 +1,51 @@
+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
new file mode 100644
index 000000000..a5d351dfa
--- /dev/null
+++ b/samples/AspNetCoreMcpServerPerUserTools/Tools/PublicTool.cs
@@ -0,0 +1,23 @@
+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
new file mode 100644
index 000000000..8f2c1b646
--- /dev/null
+++ b/samples/AspNetCoreMcpServerPerUserTools/Tools/UserTool.cs
@@ -0,0 +1,48 @@
+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
diff --git a/samples/AspNetCoreMcpServerPerUserTools/appsettings.Development.json b/samples/AspNetCoreMcpServerPerUserTools/appsettings.Development.json
new file mode 100644
index 000000000..f999bc20e
--- /dev/null
+++ b/samples/AspNetCoreMcpServerPerUserTools/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/AspNetCoreMcpServerPerUserTools/appsettings.json b/samples/AspNetCoreMcpServerPerUserTools/appsettings.json
new file mode 100644
index 000000000..a7f1eede0
--- /dev/null
+++ b/samples/AspNetCoreMcpServerPerUserTools/appsettings.json
@@ -0,0 +1,10 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning",
+ "AspNetCoreMcpServerPerUserTools": "Debug"
+ }
+ },
+ "AllowedHosts": "*"
+}
\ No newline at end of file