Skip to content

Commit caca2ae

Browse files
authored
Overhaul tool handling (#89)
* Overhaul tool handling - Renames [McpTool{Type}] to [McpServerTool{Type}], in order to distinguish representations of tools on the server from tools on the client. - Enables [McpServerTool] methods to have arguments injected from DI, as well as a specific set of types from the implementation directly, like IMcpServer. - Renames WithTools to WithToolsFromAssembly. - All of the WithToolsXx methods now publish each individual McpServerTool into DI; other code can do so as well. The options setup code gathers all of the tools from DI and combines them into a collection which is then stored in McpServerOptions and used to construct the server. - The server tools specified via DI as well as manually-provided handlers, using CallToolHandler as a fallback in case the requested tool doesn't exist in the tools collection, and ListToolsHandler augments the information for whatever tools exist. - The tools are stored in McpServerOptions in a new McpServerToolCollection type, which is a thread-safe container that exposes add/removal notification. Adding/removing tools triggers a change notification that in turn sends a notification to the client about a tools update. - The ServerOptions are exposed from the server instance. - Removed cursor-based APIs from McpClientExtensions. - Changed McpClientExtensions APIs to return `Task<IList<...>` rather than `IAsyncEnumerable<...>`. I'm sure this will need to evolve further before we're "done", but it's a significant improvement from where we are now. One area I'm not super happy with is the notifying collection; that might work ok for a stdio server, where you can grab the created collection from the server's options and mutate it, but I don't know how well that's going to work for sse servers. * Make McpServerTool{Collection} extensible * Fix README.md example * Add notifications test * Address feedback
1 parent aafa9d9 commit caca2ae

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+2703
-686
lines changed

README.MD

Lines changed: 46 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ await foreach (var tool in client.ListToolsAsync())
4444
// Execute a tool (this would normally be driven by LLM tool invocations).
4545
var result = await client.CallToolAsync(
4646
"echo",
47-
new() { ["message"] = "Hello MCP!" },
47+
new Dictionary<string, object?>() { ["message"] = "Hello MCP!" },
4848
CancellationToken.None);
4949

5050
// echo always returns one and only one text content object
@@ -59,16 +59,13 @@ Tools can be exposed easily as `AIFunction` instances so that they are immediate
5959

6060
```csharp
6161
// Get available functions.
62-
IList<AIFunction> tools = await client.GetAIFunctionsAsync();
62+
IList<McpClientTool> tools = await client.ListToolsAsync();
6363

6464
// Call the chat client using the tools.
6565
IChatClient chatClient = ...;
6666
var response = await chatClient.GetResponseAsync(
6767
"your prompt here",
68-
new()
69-
{
70-
Tools = [.. tools],
71-
});
68+
new() { Tools = [.. tools] },
7269
```
7370

7471
## Getting Started (Server)
@@ -88,17 +85,47 @@ var builder = Host.CreateEmptyApplicationBuilder(settings: null);
8885
builder.Services
8986
.AddMcpServer()
9087
.WithStdioServerTransport()
91-
.WithTools();
88+
.WithToolsFromAssembly();
9289
await builder.Build().RunAsync();
9390

94-
[McpToolType]
91+
[McpServerToolType]
9592
public static class EchoTool
9693
{
97-
[McpTool, Description("Echoes the message back to the client.")]
94+
[McpServerTool, Description("Echoes the message back to the client.")]
9895
public static string Echo(string message) => $"hello {message}";
9996
}
10097
```
10198

99+
Tools can have the `IMcpServer` representing the server injected via a parameter to the method, and can use that for interaction with
100+
the connected client. Similarly, arguments may be injected via dependency injection. For example, this tool will use the supplied
101+
`IMcpServer` to make sampling requests back to the client in order to summarize content it downloads from the specified url via
102+
an `HttpClient` injected via dependency injection.
103+
```csharp
104+
[McpServerTool("SummarizeContentFromUrl"), Description("Summarizes content downloaded from a specific URI")]
105+
public static async Task<string> SummarizeDownloadedContent(
106+
IMcpServer thisServer,
107+
HttpClient httpClient,
108+
[Description("The url from which to download the content to summarize")] string url,
109+
CancellationToken cancellationToken)
110+
{
111+
string content = await httpClient.GetStringAsync(url);
112+
113+
ChatMessage[] messages =
114+
[
115+
new(ChatRole.User, "Briefly summarize the following downloaded content:"),
116+
new(ChatRole.User, content),
117+
]
118+
119+
ChatOptions options = new()
120+
{
121+
MaxOutputTokens = 256,
122+
Temperature = 0.3f,
123+
};
124+
125+
return $"Summary: {await thisServer.AsSamplingChatClient().GetResponseAsync(messages, options, cancellationToken)}";
126+
}
127+
```
128+
102129
More control is also available, with fine-grained control over configuring the server and how it should handle client requests. For example:
103130

104131
```csharp
@@ -124,14 +151,18 @@ McpServerOptions options = new()
124151
{
125152
Name = "echo",
126153
Description = "Echoes the input back to the client.",
127-
InputSchema = new JsonSchema()
128-
{
129-
Type = "object",
130-
Properties = new Dictionary<string, JsonSchemaProperty>()
154+
InputSchema = JsonSerializer.Deserialize<JsonElement>("""
131155
{
132-
["message"] = new JsonSchemaProperty() { Type = "string", Description = "The input to echo back." }
156+
"type": "object",
157+
"properties": {
158+
"message": {
159+
"type": "string",
160+
"description": "The input to echo back"
161+
}
162+
},
163+
"required": ["message"]
133164
}
134-
},
165+
"""),
135166
}
136167
]
137168
};

samples/AspNetCoreSseServer/Program.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
using AspNetCoreSseServer;
33

44
var builder = WebApplication.CreateBuilder(args);
5-
builder.Services.AddMcpServer().WithTools();
5+
builder.Services.AddMcpServer().WithToolsFromAssembly();
66
var app = builder.Build();
77

88
app.MapGet("/", () => "Hello World!");

samples/AspNetCoreSseServer/Tools/EchoTool.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33

44
namespace TestServerWithHosting.Tools;
55

6-
[McpToolType]
6+
[McpServerToolType]
77
public static class EchoTool
88
{
9-
[McpTool, Description("Echoes the input back to the client.")]
9+
[McpServerTool, Description("Echoes the input back to the client.")]
1010
public static string Echo(string message)
1111
{
1212
return "hello " + message;
Lines changed: 18 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,36 @@
1-
using ModelContextProtocol.Protocol.Types;
1+
using Microsoft.Extensions.AI;
22
using ModelContextProtocol.Server;
33
using System.ComponentModel;
44

55
namespace TestServerWithHosting.Tools;
66

77
/// <summary>
8-
/// This tool uses depenency injection and async method
8+
/// This tool uses dependency injection and async method
99
/// </summary>
10-
[McpToolType]
11-
public class SampleLlmTool
10+
[McpServerToolType]
11+
public static class SampleLlmTool
1212
{
13-
private readonly IMcpServer _server;
14-
15-
public SampleLlmTool(IMcpServer server)
16-
{
17-
_server = server ?? throw new ArgumentNullException(nameof(server));
18-
}
19-
20-
[McpTool("sampleLLM"), Description("Samples from an LLM using MCP's sampling feature")]
21-
public async Task<string> SampleLLM(
13+
[McpServerTool("sampleLLM"), Description("Samples from an LLM using MCP's sampling feature")]
14+
public static async Task<string> SampleLLM(
15+
IMcpServer thisServer,
2216
[Description("The prompt to send to the LLM")] string prompt,
2317
[Description("Maximum number of tokens to generate")] int maxTokens,
2418
CancellationToken cancellationToken)
2519
{
26-
var samplingParams = CreateRequestSamplingParams(prompt ?? string.Empty, "sampleLLM", maxTokens);
27-
var sampleResult = await _server.RequestSamplingAsync(samplingParams, cancellationToken);
20+
ChatMessage[] messages =
21+
[
22+
new(ChatRole.System, "You are a helpful test server."),
23+
new(ChatRole.User, prompt),
24+
];
2825

29-
return $"LLM sampling result: {sampleResult.Content.Text}";
30-
}
31-
32-
private static CreateMessageRequestParams CreateRequestSamplingParams(string context, string uri, int maxTokens = 100)
33-
{
34-
return new CreateMessageRequestParams()
26+
ChatOptions options = new()
3527
{
36-
Messages = [new SamplingMessage()
37-
{
38-
Role = Role.User,
39-
Content = new Content()
40-
{
41-
Type = "text",
42-
Text = $"Resource {uri} context: {context}"
43-
}
44-
}],
45-
SystemPrompt = "You are a helpful test server.",
46-
MaxTokens = maxTokens,
28+
MaxOutputTokens = maxTokens,
4729
Temperature = 0.7f,
48-
IncludeContext = ContextInclusion.ThisServer
4930
};
31+
32+
var samplingResponse = await thisServer.AsSamplingChatClient().GetResponseAsync(messages, options, cancellationToken);
33+
34+
return $"LLM sampling result: {samplingResponse}";
5035
}
5136
}

samples/ChatWithTools/ChatWithTools.csproj

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
<PackageReference Include="Microsoft.Extensions.AI" />
1212
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
1313
<PackageReference Include="Anthropic.SDK" />
14-
<PackageReference Include="System.Linq.AsyncEnumerable" />
1514
</ItemGroup>
1615

1716
<ItemGroup>

samples/ChatWithTools/Program.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

2020
// Get all available tools
2121
Console.WriteLine("Tools available:");
22-
var tools = await mcpClient.GetAIFunctionsAsync();
22+
var tools = await mcpClient.ListToolsAsync();
2323
foreach (var tool in tools)
2424
{
2525
Console.WriteLine($" {tool}");

samples/TestServerWithHosting/Program.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
builder.Services.AddSerilog();
2020
builder.Services.AddMcpServer()
2121
.WithStdioServerTransport()
22-
.WithTools();
22+
.WithToolsFromAssembly();
2323

2424
var app = builder.Build();
2525

samples/TestServerWithHosting/Tools/EchoTool.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33

44
namespace TestServerWithHosting.Tools;
55

6-
[McpToolType]
6+
[McpServerToolType]
77
public static class EchoTool
88
{
9-
[McpTool, Description("Echoes the input back to the client.")]
9+
[McpServerTool, Description("Echoes the input back to the client.")]
1010
public static string Echo(string message)
1111
{
1212
return "hello " + message;

samples/TestServerWithHosting/Tools/SampleLlmTool.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ namespace TestServerWithHosting.Tools;
77
/// <summary>
88
/// This tool uses depenency injection and async method
99
/// </summary>
10-
[McpToolType]
10+
[McpServerToolType]
1111
public class SampleLlmTool
1212
{
1313
private readonly IMcpServer _server;
@@ -17,7 +17,7 @@ public SampleLlmTool(IMcpServer server)
1717
_server = server ?? throw new ArgumentNullException(nameof(server));
1818
}
1919

20-
[McpTool("sampleLLM"), Description("Samples from an LLM using MCP's sampling feature")]
20+
[McpServerTool("sampleLLM"), Description("Samples from an LLM using MCP's sampling feature")]
2121
public async Task<string> SampleLLM(
2222
[Description("The prompt to send to the LLM")] string prompt,
2323
[Description("Maximum number of tokens to generate")] int maxTokens,

src/Common/Polyfills/System/Collections/Generic/CollectionExtensions.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,7 @@ public static TValue GetValueOrDefault<TKey, TValue>(this IReadOnlyDictionary<TK
1515

1616
return dictionary.TryGetValue(key, out TValue? value) ? value : defaultValue;
1717
}
18+
19+
public static Dictionary<TKey, TValue> ToDictionary<TKey, TValue>(this IEnumerable<KeyValuePair<TKey, TValue>> source) =>
20+
source.ToDictionary(kv => kv.Key, kv => kv.Value);
1821
}

0 commit comments

Comments
 (0)