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
This commit is contained in:
parent
aafa9d929d
commit
caca2ae61d
1 changed files with 2703 additions and 686 deletions
61
README.MD
61
README.MD
|
|
@ -44,7 +44,7 @@ await foreach (var tool in client.ListToolsAsync())
|
|||
// Execute a tool (this would normally be driven by LLM tool invocations).
|
||||
var result = await client.CallToolAsync(
|
||||
"echo",
|
||||
new() { ["message"] = "Hello MCP!" },
|
||||
new Dictionary<string, object?>() { ["message"] = "Hello MCP!" },
|
||||
CancellationToken.None);
|
||||
|
||||
// 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
|
|||
|
||||
```csharp
|
||||
// Get available functions.
|
||||
IList<AIFunction> tools = await client.GetAIFunctionsAsync();
|
||||
IList<McpClientTool> tools = await client.ListToolsAsync();
|
||||
|
||||
// Call the chat client using the tools.
|
||||
IChatClient chatClient = ...;
|
||||
var response = await chatClient.GetResponseAsync(
|
||||
"your prompt here",
|
||||
new()
|
||||
{
|
||||
Tools = [.. tools],
|
||||
});
|
||||
new() { Tools = [.. tools] },
|
||||
```
|
||||
|
||||
## Getting Started (Server)
|
||||
|
|
@ -88,17 +85,47 @@ var builder = Host.CreateEmptyApplicationBuilder(settings: null);
|
|||
builder.Services
|
||||
.AddMcpServer()
|
||||
.WithStdioServerTransport()
|
||||
.WithTools();
|
||||
.WithToolsFromAssembly();
|
||||
await builder.Build().RunAsync();
|
||||
|
||||
[McpToolType]
|
||||
[McpServerToolType]
|
||||
public static class EchoTool
|
||||
{
|
||||
[McpTool, Description("Echoes the message back to the client.")]
|
||||
[McpServerTool, Description("Echoes the message back to the client.")]
|
||||
public static string Echo(string message) => $"hello {message}";
|
||||
}
|
||||
```
|
||||
|
||||
Tools can have the `IMcpServer` representing the server injected via a parameter to the method, and can use that for interaction with
|
||||
the connected client. Similarly, arguments may be injected via dependency injection. For example, this tool will use the supplied
|
||||
`IMcpServer` to make sampling requests back to the client in order to summarize content it downloads from the specified url via
|
||||
an `HttpClient` injected via dependency injection.
|
||||
```csharp
|
||||
[McpServerTool("SummarizeContentFromUrl"), Description("Summarizes content downloaded from a specific URI")]
|
||||
public static async Task<string> SummarizeDownloadedContent(
|
||||
IMcpServer thisServer,
|
||||
HttpClient httpClient,
|
||||
[Description("The url from which to download the content to summarize")] string url,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
string content = await httpClient.GetStringAsync(url);
|
||||
|
||||
ChatMessage[] messages =
|
||||
[
|
||||
new(ChatRole.User, "Briefly summarize the following downloaded content:"),
|
||||
new(ChatRole.User, content),
|
||||
]
|
||||
|
||||
ChatOptions options = new()
|
||||
{
|
||||
MaxOutputTokens = 256,
|
||||
Temperature = 0.3f,
|
||||
};
|
||||
|
||||
return $"Summary: {await thisServer.AsSamplingChatClient().GetResponseAsync(messages, options, cancellationToken)}";
|
||||
}
|
||||
```
|
||||
|
||||
More control is also available, with fine-grained control over configuring the server and how it should handle client requests. For example:
|
||||
|
||||
```csharp
|
||||
|
|
@ -124,14 +151,18 @@ McpServerOptions options = new()
|
|||
{
|
||||
Name = "echo",
|
||||
Description = "Echoes the input back to the client.",
|
||||
InputSchema = new JsonSchema()
|
||||
{
|
||||
Type = "object",
|
||||
Properties = new Dictionary<string, JsonSchemaProperty>()
|
||||
InputSchema = JsonSerializer.Deserialize<JsonElement>("""
|
||||
{
|
||||
["message"] = new JsonSchemaProperty() { Type = "string", Description = "The input to echo back." }
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string",
|
||||
"description": "The input to echo back"
|
||||
}
|
||||
},
|
||||
"required": ["message"]
|
||||
}
|
||||
},
|
||||
"""),
|
||||
}
|
||||
]
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue