Skip to content

Commit 0f0aad0

Browse files
authored
Per Session Tools Sample (#724)
1 parent ec3b08b commit 0f0aad0

File tree

10 files changed

+396
-0
lines changed

10 files changed

+396
-0
lines changed

ModelContextProtocol.slnx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
<Project Path="docs/concepts/progress/samples/server/Progress.csproj" />
3636
</Folder>
3737
<Folder Name="/samples/">
38+
<Project Path="samples/AspNetCoreMcpPerSessionTools/AspNetCoreMcpPerSessionTools.csproj" />
3839
<Project Path="samples/AspNetCoreMcpServer/AspNetCoreMcpServer.csproj" />
3940
<Project Path="samples/ChatWithTools/ChatWithTools.csproj" />
4041
<Project Path="samples/EverythingServer/EverythingServer.csproj" />
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
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.Extensions.Hosting" />
16+
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" />
17+
<PackageReference Include="OpenTelemetry.Instrumentation.Http" />
18+
</ItemGroup>
19+
20+
</Project>
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
using AspNetCoreMcpPerSessionTools.Tools;
2+
using ModelContextProtocol.Server;
3+
using System.Collections.Concurrent;
4+
using System.Reflection;
5+
6+
var builder = WebApplication.CreateBuilder(args);
7+
8+
// Create and populate the tool dictionary at startup
9+
var toolDictionary = new ConcurrentDictionary<string, McpServerTool[]>();
10+
PopulateToolDictionary(toolDictionary);
11+
12+
// Register all MCP server tools - they will be filtered per session based on route
13+
builder.Services.AddMcpServer()
14+
.WithHttpTransport(options =>
15+
{
16+
// Configure per-session options to filter tools based on route category
17+
options.ConfigureSessionOptions = async (httpContext, mcpOptions, cancellationToken) =>
18+
{
19+
// Determine tool category from route parameters
20+
var toolCategory = httpContext.Request.RouteValues["toolCategory"]?.ToString()?.ToLower() ?? "all";
21+
22+
// Get pre-populated tools for the requested category
23+
if (toolDictionary.TryGetValue(toolCategory, out var tools))
24+
{
25+
mcpOptions.Capabilities = new();
26+
mcpOptions.Capabilities.Tools = new();
27+
var toolCollection = mcpOptions.Capabilities.Tools.ToolCollection = new();
28+
29+
foreach (var tool in tools)
30+
{
31+
toolCollection.Add(tool);
32+
}
33+
}
34+
};
35+
});
36+
37+
var app = builder.Build();
38+
39+
// Map MCP with route parameter for tool category filtering
40+
app.MapMcp("/{toolCategory?}");
41+
42+
app.Run();
43+
44+
// Helper method to populate the tool dictionary at startup
45+
static void PopulateToolDictionary(ConcurrentDictionary<string, McpServerTool[]> toolDictionary)
46+
{
47+
// Get tools for each category
48+
var clockTools = GetToolsForType<ClockTool>();
49+
var calculatorTools = GetToolsForType<CalculatorTool>();
50+
var userInfoTools = GetToolsForType<UserInfoTool>();
51+
McpServerTool[] allTools = [.. clockTools,
52+
.. calculatorTools,
53+
.. userInfoTools];
54+
55+
// Populate the dictionary with tools for each category
56+
toolDictionary.TryAdd("clock", clockTools);
57+
toolDictionary.TryAdd("calculator", calculatorTools);
58+
toolDictionary.TryAdd("userinfo", userInfoTools);
59+
toolDictionary.TryAdd("all", allTools);
60+
}
61+
62+
// Helper method to get tools for a specific type using reflection
63+
static McpServerTool[] GetToolsForType<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(
64+
System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicMethods)] T>()
65+
{
66+
var tools = new List<McpServerTool>();
67+
var toolType = typeof(T);
68+
var methods = toolType.GetMethods(BindingFlags.Public | BindingFlags.Static)
69+
.Where(m => m.GetCustomAttributes(typeof(McpServerToolAttribute), false).Any());
70+
71+
foreach (var method in methods)
72+
{
73+
try
74+
{
75+
var tool = McpServerTool.Create(method, target: null, new McpServerToolCreateOptions());
76+
tools.Add(tool);
77+
}
78+
catch (Exception ex)
79+
{
80+
// Log error but continue with other tools
81+
Console.WriteLine($"Failed to add tool {toolType.Name}.{method.Name}: {ex.Message}");
82+
}
83+
}
84+
85+
return [.. tools];
86+
}
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+
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
# ASP.NET Core MCP Server with Per-Session Tool Filtering
2+
3+
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.
4+
5+
## Overview
6+
7+
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.
8+
9+
## Route-Based Configuration
10+
11+
The server uses route parameters to determine which tools to make available:
12+
13+
- `GET /clock` - MCP server with only clock/time tools
14+
- `GET /calculator` - MCP server with only calculation tools
15+
- `GET /userinfo` - MCP server with only session/system info tools
16+
- `GET /all` or `GET /` - MCP server with all tools (default)
17+
18+
## Running the Sample
19+
20+
1. Navigate to the sample directory:
21+
```bash
22+
cd samples/AspNetCoreMcpPerSessionTools
23+
```
24+
25+
2. Run the server:
26+
```bash
27+
dotnet run
28+
```
29+
30+
3. The server will start on `https://localhost:5001` (or the port shown in the console)
31+
32+
## Testing Tool Categories
33+
34+
### Testing Clock Tools
35+
Connect your MCP client to: `https://localhost:5001/clock`
36+
- Available tools: GetTime, GetDate, ConvertTimeZone
37+
38+
### Testing Calculator Tools
39+
Connect your MCP client to: `https://localhost:5001/calculator`
40+
- Available tools: Calculate, CalculatePercentage, SquareRoot
41+
42+
### Testing UserInfo Tools
43+
Connect your MCP client to: `https://localhost:5001/userinfo`
44+
- Available tools: GetUserInfo
45+
46+
### Testing All Tools
47+
Connect your MCP client to: `https://localhost:5001/all` or `https://localhost:5001/`
48+
- Available tools: All tools from all categories
49+
50+
## How It Works
51+
52+
### 1. Tool Registration
53+
All tools are registered during startup using the normal MCP tool registration:
54+
55+
```csharp
56+
builder.Services.AddMcpServer()
57+
.WithTools<ClockTool>()
58+
.WithTools<CalculatorTool>()
59+
.WithTools<UserInfoTool>();
60+
```
61+
62+
### 2. Route-Based Session Filtering
63+
The key technique is using `ConfigureSessionOptions` to modify the tool collection per session based on the route:
64+
65+
```csharp
66+
.WithHttpTransport(options =>
67+
{
68+
options.ConfigureSessionOptions = async (httpContext, mcpOptions, cancellationToken) =>
69+
{
70+
var toolCategory = GetToolCategoryFromRoute(httpContext);
71+
var toolCollection = mcpOptions.Capabilities?.Tools?.ToolCollection;
72+
73+
if (toolCollection != null)
74+
{
75+
// Clear all tools and add back only those for this category
76+
toolCollection.Clear();
77+
78+
switch (toolCategory?.ToLower())
79+
{
80+
case "clock":
81+
AddToolsForType<ClockTool>(toolCollection);
82+
break;
83+
case "calculator":
84+
AddToolsForType<CalculatorTool>(toolCollection);
85+
break;
86+
case "userinfo":
87+
AddToolsForType<UserInfoTool>(toolCollection);
88+
break;
89+
default:
90+
// All tools for default/all category
91+
AddToolsForType<ClockTool>(toolCollection);
92+
AddToolsForType<CalculatorTool>(toolCollection);
93+
AddToolsForType<UserInfoTool>(toolCollection);
94+
break;
95+
}
96+
}
97+
};
98+
})
99+
```
100+
101+
### 3. Route Parameter Detection
102+
The `GetToolCategoryFromRoute` method extracts the tool category from the URL route:
103+
104+
```csharp
105+
static string? GetToolCategoryFromRoute(HttpContext context)
106+
{
107+
if (context.Request.RouteValues.TryGetValue("toolCategory", out var categoryObj) && categoryObj is string category)
108+
{
109+
return string.IsNullOrEmpty(category) ? "all" : category;
110+
}
111+
return "all"; // Default
112+
}
113+
```
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
using ModelContextProtocol.Server;
2+
using System.ComponentModel;
3+
4+
namespace AspNetCoreMcpPerSessionTools.Tools;
5+
6+
/// <summary>
7+
/// Calculator tools for mathematical operations
8+
/// </summary>
9+
[McpServerToolType]
10+
public sealed class CalculatorTool
11+
{
12+
[McpServerTool, Description("Performs basic arithmetic calculations (addition, subtraction, multiplication, division).")]
13+
public static string Calculate([Description("Mathematical expression to evaluate (e.g., '5 + 3', '10 - 2', '4 * 6', '15 / 3')")] string expression)
14+
{
15+
try
16+
{
17+
// Simple calculator for demo purposes - supports basic operations
18+
expression = expression.Trim();
19+
20+
if (expression.Contains("+"))
21+
{
22+
var parts = expression.Split('+');
23+
if (parts.Length == 2 && double.TryParse(parts[0].Trim(), out var a) && double.TryParse(parts[1].Trim(), out var b))
24+
{
25+
return $"{expression} = {a + b}";
26+
}
27+
}
28+
else if (expression.Contains("-"))
29+
{
30+
var parts = expression.Split('-');
31+
if (parts.Length == 2 && double.TryParse(parts[0].Trim(), out var a) && double.TryParse(parts[1].Trim(), out var b))
32+
{
33+
return $"{expression} = {a - b}";
34+
}
35+
}
36+
else if (expression.Contains("*"))
37+
{
38+
var parts = expression.Split('*');
39+
if (parts.Length == 2 && double.TryParse(parts[0].Trim(), out var a) && double.TryParse(parts[1].Trim(), out var b))
40+
{
41+
return $"{expression} = {a * b}";
42+
}
43+
}
44+
else if (expression.Contains("/"))
45+
{
46+
var parts = expression.Split('/');
47+
if (parts.Length == 2 && double.TryParse(parts[0].Trim(), out var a) && double.TryParse(parts[1].Trim(), out var b))
48+
{
49+
if (b == 0)
50+
return "Error: Division by zero";
51+
return $"{expression} = {a / b}";
52+
}
53+
}
54+
55+
return $"Cannot evaluate expression: {expression}. Supported operations: +, -, *, / (e.g., '5 + 3')";
56+
}
57+
catch (Exception ex)
58+
{
59+
return $"Error evaluating '{expression}': {ex.Message}";
60+
}
61+
}
62+
63+
[McpServerTool, Description("Calculates percentage of a number.")]
64+
public static string CalculatePercentage(
65+
[Description("The number to calculate percentage of")] double number,
66+
[Description("The percentage value")] double percentage)
67+
{
68+
var result = (number * percentage) / 100;
69+
return $"{percentage}% of {number} = {result}";
70+
}
71+
72+
[McpServerTool, Description("Calculates the square root of a number.")]
73+
public static string SquareRoot([Description("The number to find square root of")] double number)
74+
{
75+
if (number < 0)
76+
return "Error: Cannot calculate square root of negative number";
77+
78+
var result = Math.Sqrt(number);
79+
return $"√{number} = {result}";
80+
}
81+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
using ModelContextProtocol.Server;
2+
using System.ComponentModel;
3+
4+
namespace AspNetCoreMcpPerSessionTools.Tools;
5+
6+
/// <summary>
7+
/// Clock-related tools for time and date operations
8+
/// </summary>
9+
[McpServerToolType]
10+
public sealed class ClockTool
11+
{
12+
[McpServerTool, Description("Gets the current server time in various formats.")]
13+
public static string GetTime()
14+
{
15+
return $"Current server time: {DateTime.Now:yyyy-MM-dd HH:mm:ss} UTC";
16+
}
17+
18+
[McpServerTool, Description("Gets the current date in a specific format.")]
19+
public static string GetDate([Description("Date format (e.g., 'yyyy-MM-dd', 'MM/dd/yyyy')")] string format = "yyyy-MM-dd")
20+
{
21+
try
22+
{
23+
return $"Current date: {DateTime.Now.ToString(format)}";
24+
}
25+
catch (FormatException)
26+
{
27+
return $"Invalid format '{format}'. Using default: {DateTime.Now:yyyy-MM-dd}";
28+
}
29+
}
30+
31+
[McpServerTool, Description("Converts time between timezones.")]
32+
public static string ConvertTimeZone(
33+
[Description("Source timezone (e.g., 'UTC', 'EST')")] string fromTimeZone = "UTC",
34+
[Description("Target timezone (e.g., 'PST', 'GMT')")] string toTimeZone = "PST")
35+
{
36+
// Simplified timezone conversion for demo purposes
37+
var now = DateTime.Now;
38+
return $"Time conversion from {fromTimeZone} to {toTimeZone}: {now:HH:mm:ss} (simulated)";
39+
}
40+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
using ModelContextProtocol.Server;
2+
using System.ComponentModel;
3+
4+
namespace AspNetCoreMcpPerSessionTools.Tools;
5+
6+
/// <summary>
7+
/// User information tools
8+
/// </summary>
9+
[McpServerToolType]
10+
public sealed class UserInfoTool
11+
{
12+
[McpServerTool, Description("Gets information about the current user in the MCP session.")]
13+
public static string GetUserInfo()
14+
{
15+
// Dummy user information for demonstration purposes
16+
return $"User Information:\n" +
17+
$"- User ID: {Guid.NewGuid():N}[..8] (simulated)\n" +
18+
$"- Username: User{new Random().Next(1, 1000)}\n" +
19+
$"- Roles: User, Guest\n" +
20+
$"- Last Login: {DateTime.Now.AddMinutes(-new Random().Next(1, 60)):HH:mm:ss}\n" +
21+
$"- Account Status: Active";
22+
}
23+
}

0 commit comments

Comments
 (0)