Skip to content

Commit 774c752

Browse files
authored
Merge pull request #1 from PederHP/copilot/fix-a0ca257f-b665-488b-a76b-c4b4779d276e
Add ASP.NET Core MCP server sample showcasing per-user tool filtering
2 parents 52b1ed5 + aa88dc7 commit 774c752

File tree

9 files changed

+581
-0
lines changed

9 files changed

+581
-0
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<Project Sdk="Microsoft.NET.Sdk.Web">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net9.0</TargetFramework>
5+
<Nullable>enable</Nullable>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<PublishAot>true</PublishAot>
8+
</PropertyGroup>
9+
10+
<ItemGroup>
11+
<ProjectReference Include="..\..\src\ModelContextProtocol.AspNetCore\ModelContextProtocol.AspNetCore.csproj" />
12+
</ItemGroup>
13+
14+
<ItemGroup>
15+
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" />
16+
<PackageReference Include="OpenTelemetry.Extensions.Hosting" />
17+
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" />
18+
<PackageReference Include="OpenTelemetry.Instrumentation.Http" />
19+
</ItemGroup>
20+
21+
</Project>
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
using OpenTelemetry;
2+
using OpenTelemetry.Metrics;
3+
using OpenTelemetry.Trace;
4+
using AspNetCoreMcpServerPerUserTools.Tools;
5+
using ModelContextProtocol.Server;
6+
7+
var builder = WebApplication.CreateBuilder(args);
8+
9+
// Register all MCP server tools - they will be filtered per user later
10+
builder.Services.AddMcpServer()
11+
.WithHttpTransport(options =>
12+
{
13+
// Configure per-session options to filter tools based on user permissions
14+
options.ConfigureSessionOptions = async (httpContext, mcpOptions, cancellationToken) =>
15+
{
16+
// Determine user role from headers (in real apps, use proper authentication)
17+
var userRole = GetUserRole(httpContext);
18+
var userId = GetUserId(httpContext);
19+
20+
// Get the tool collection that we can modify per session
21+
var toolCollection = mcpOptions.Capabilities?.Tools?.ToolCollection;
22+
if (toolCollection != null)
23+
{
24+
// Clear all tools first
25+
toolCollection.Clear();
26+
27+
// Add tools based on user role
28+
switch (userRole)
29+
{
30+
case "admin":
31+
// Admins get all tools
32+
AddToolsForType<PublicTool>(toolCollection);
33+
AddToolsForType<UserTool>(toolCollection);
34+
AddToolsForType<AdminTool>(toolCollection);
35+
break;
36+
37+
case "user":
38+
// Regular users get public and user tools
39+
AddToolsForType<PublicTool>(toolCollection);
40+
AddToolsForType<UserTool>(toolCollection);
41+
break;
42+
43+
default:
44+
// Anonymous/public users get only public tools
45+
AddToolsForType<PublicTool>(toolCollection);
46+
break;
47+
}
48+
}
49+
50+
// Optional: Log the session configuration for debugging
51+
var logger = httpContext.RequestServices.GetRequiredService<ILogger<Program>>();
52+
logger.LogInformation("Configured MCP session for user {UserId} with role {UserRole}, {ToolCount} tools available",
53+
userId, userRole, toolCollection?.Count ?? 0);
54+
};
55+
})
56+
.WithTools<PublicTool>()
57+
.WithTools<UserTool>()
58+
.WithTools<AdminTool>();
59+
60+
// Add OpenTelemetry for observability
61+
builder.Services.AddOpenTelemetry()
62+
.WithTracing(b => b.AddSource("*")
63+
.AddAspNetCoreInstrumentation()
64+
.AddHttpClientInstrumentation())
65+
.WithMetrics(b => b.AddMeter("*")
66+
.AddAspNetCoreInstrumentation()
67+
.AddHttpClientInstrumentation())
68+
.WithLogging()
69+
.UseOtlpExporter();
70+
71+
var app = builder.Build();
72+
73+
// Add middleware to log requests for demo purposes
74+
app.Use(async (context, next) =>
75+
{
76+
var logger = context.RequestServices.GetRequiredService<ILogger<Program>>();
77+
var userRole = GetUserRole(context);
78+
var userId = GetUserId(context);
79+
80+
logger.LogInformation("Request from User {UserId} with Role {UserRole}: {Method} {Path}",
81+
userId, userRole, context.Request.Method, context.Request.Path);
82+
83+
await next();
84+
});
85+
86+
app.MapMcp();
87+
88+
// Add a simple endpoint to test authentication headers
89+
app.MapGet("/test-auth", (HttpContext context) =>
90+
{
91+
var userRole = GetUserRole(context);
92+
var userId = GetUserId(context);
93+
94+
return Results.Text($"UserId: {userId}\nRole: {userRole}\nMessage: You are authenticated as {userId} with role {userRole}");
95+
});
96+
97+
app.Run();
98+
99+
// Helper methods for authentication - in production, use proper authentication/authorization
100+
static string GetUserRole(HttpContext context)
101+
{
102+
// Check for X-User-Role header first
103+
if (context.Request.Headers.TryGetValue("X-User-Role", out var roleHeader))
104+
{
105+
var role = roleHeader.ToString().ToLowerInvariant();
106+
if (role is "admin" or "user" or "public")
107+
{
108+
return role;
109+
}
110+
}
111+
112+
// Check for Authorization header pattern (Bearer token simulation)
113+
if (context.Request.Headers.TryGetValue("Authorization", out var authHeader))
114+
{
115+
var auth = authHeader.ToString();
116+
if (auth.StartsWith("Bearer admin-", StringComparison.OrdinalIgnoreCase))
117+
return "admin";
118+
if (auth.StartsWith("Bearer user-", StringComparison.OrdinalIgnoreCase))
119+
return "user";
120+
if (auth.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
121+
return "public";
122+
}
123+
124+
// Default to public access
125+
return "public";
126+
}
127+
128+
static string GetUserId(HttpContext context)
129+
{
130+
// Check for X-User-Id header first
131+
if (context.Request.Headers.TryGetValue("X-User-Id", out var userIdHeader))
132+
{
133+
return userIdHeader.ToString();
134+
}
135+
136+
// Extract from Authorization header if present
137+
if (context.Request.Headers.TryGetValue("Authorization", out var authHeader))
138+
{
139+
var auth = authHeader.ToString();
140+
if (auth.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
141+
{
142+
var token = auth["Bearer ".Length..];
143+
return token.Contains('-') ? token : $"user-{token}";
144+
}
145+
}
146+
147+
// Generate anonymous ID
148+
return $"anonymous-{Guid.NewGuid():N}"[..16];
149+
}
150+
151+
static void AddToolsForType<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(
152+
System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicMethods)]T>(
153+
McpServerPrimitiveCollection<McpServerTool> toolCollection)
154+
{
155+
var toolType = typeof(T);
156+
var methods = toolType.GetMethods(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static)
157+
.Where(m => m.GetCustomAttributes(typeof(McpServerToolAttribute), false).Any());
158+
159+
foreach (var method in methods)
160+
{
161+
try
162+
{
163+
var tool = McpServerTool.Create(method, target: null, new McpServerToolCreateOptions());
164+
toolCollection.Add(tool);
165+
}
166+
catch (Exception ex)
167+
{
168+
// Log error but continue with other tools
169+
Console.WriteLine($"Failed to add tool {toolType.Name}.{method.Name}: {ex.Message}");
170+
}
171+
}
172+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"profiles": {
3+
"http": {
4+
"commandName": "Project",
5+
"dotnetRunMessages": true,
6+
"launchBrowser": false,
7+
"applicationUrl": "http://localhost:3001",
8+
"environmentVariables": {
9+
"ASPNETCORE_ENVIRONMENT": "Development"
10+
}
11+
}
12+
}
13+
}

0 commit comments

Comments
 (0)