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
47 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).
|
// Execute a tool (this would normally be driven by LLM tool invocations).
|
||||||
var result = await client.CallToolAsync(
|
var result = await client.CallToolAsync(
|
||||||
"echo",
|
"echo",
|
||||||
new() { ["message"] = "Hello MCP!" },
|
new Dictionary<string, object?>() { ["message"] = "Hello MCP!" },
|
||||||
CancellationToken.None);
|
CancellationToken.None);
|
||||||
|
|
||||||
// echo always returns one and only one text content object
|
// 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
|
```csharp
|
||||||
// Get available functions.
|
// Get available functions.
|
||||||
IList<AIFunction> tools = await client.GetAIFunctionsAsync();
|
IList<McpClientTool> tools = await client.ListToolsAsync();
|
||||||
|
|
||||||
// Call the chat client using the tools.
|
// Call the chat client using the tools.
|
||||||
IChatClient chatClient = ...;
|
IChatClient chatClient = ...;
|
||||||
var response = await chatClient.GetResponseAsync(
|
var response = await chatClient.GetResponseAsync(
|
||||||
"your prompt here",
|
"your prompt here",
|
||||||
new()
|
new() { Tools = [.. tools] },
|
||||||
{
|
|
||||||
Tools = [.. tools],
|
|
||||||
});
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Getting Started (Server)
|
## Getting Started (Server)
|
||||||
|
|
@ -88,17 +85,47 @@ var builder = Host.CreateEmptyApplicationBuilder(settings: null);
|
||||||
builder.Services
|
builder.Services
|
||||||
.AddMcpServer()
|
.AddMcpServer()
|
||||||
.WithStdioServerTransport()
|
.WithStdioServerTransport()
|
||||||
.WithTools();
|
.WithToolsFromAssembly();
|
||||||
await builder.Build().RunAsync();
|
await builder.Build().RunAsync();
|
||||||
|
|
||||||
[McpToolType]
|
[McpServerToolType]
|
||||||
public static class EchoTool
|
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}";
|
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:
|
More control is also available, with fine-grained control over configuring the server and how it should handle client requests. For example:
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
|
|
@ -124,14 +151,18 @@ McpServerOptions options = new()
|
||||||
{
|
{
|
||||||
Name = "echo",
|
Name = "echo",
|
||||||
Description = "Echoes the input back to the client.",
|
Description = "Echoes the input back to the client.",
|
||||||
InputSchema = new JsonSchema()
|
InputSchema = JsonSerializer.Deserialize<JsonElement>("""
|
||||||
{
|
|
||||||
Type = "object",
|
|
||||||
Properties = new Dictionary<string, JsonSchemaProperty>()
|
|
||||||
{
|
{
|
||||||
["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"]
|
||||||
}
|
}
|
||||||
},
|
"""),
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ using ModelContextProtocol;
|
||||||
using AspNetCoreSseServer;
|
using AspNetCoreSseServer;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
builder.Services.AddMcpServer().WithTools();
|
builder.Services.AddMcpServer().WithToolsFromAssembly();
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
app.MapGet("/", () => "Hello World!");
|
app.MapGet("/", () => "Hello World!");
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,10 @@ using System.ComponentModel;
|
||||||
|
|
||||||
namespace TestServerWithHosting.Tools;
|
namespace TestServerWithHosting.Tools;
|
||||||
|
|
||||||
[McpToolType]
|
[McpServerToolType]
|
||||||
public static class EchoTool
|
public static class EchoTool
|
||||||
{
|
{
|
||||||
[McpTool, Description("Echoes the input back to the client.")]
|
[McpServerTool, Description("Echoes the input back to the client.")]
|
||||||
public static string Echo(string message)
|
public static string Echo(string message)
|
||||||
{
|
{
|
||||||
return "hello " + message;
|
return "hello " + message;
|
||||||
|
|
|
||||||
|
|
@ -1,51 +1,36 @@
|
||||||
using ModelContextProtocol.Protocol.Types;
|
using Microsoft.Extensions.AI;
|
||||||
using ModelContextProtocol.Server;
|
using ModelContextProtocol.Server;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
|
|
||||||
namespace TestServerWithHosting.Tools;
|
namespace TestServerWithHosting.Tools;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// This tool uses depenency injection and async method
|
/// This tool uses dependency injection and async method
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[McpToolType]
|
[McpServerToolType]
|
||||||
public class SampleLlmTool
|
public static class SampleLlmTool
|
||||||
{
|
{
|
||||||
private readonly IMcpServer _server;
|
[McpServerTool("sampleLLM"), Description("Samples from an LLM using MCP's sampling feature")]
|
||||||
|
public static async Task<string> SampleLLM(
|
||||||
public SampleLlmTool(IMcpServer server)
|
IMcpServer thisServer,
|
||||||
{
|
|
||||||
_server = server ?? throw new ArgumentNullException(nameof(server));
|
|
||||||
}
|
|
||||||
|
|
||||||
[McpTool("sampleLLM"), Description("Samples from an LLM using MCP's sampling feature")]
|
|
||||||
public async Task<string> SampleLLM(
|
|
||||||
[Description("The prompt to send to the LLM")] string prompt,
|
[Description("The prompt to send to the LLM")] string prompt,
|
||||||
[Description("Maximum number of tokens to generate")] int maxTokens,
|
[Description("Maximum number of tokens to generate")] int maxTokens,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var samplingParams = CreateRequestSamplingParams(prompt ?? string.Empty, "sampleLLM", maxTokens);
|
ChatMessage[] messages =
|
||||||
var sampleResult = await _server.RequestSamplingAsync(samplingParams, cancellationToken);
|
[
|
||||||
|
new(ChatRole.System, "You are a helpful test server."),
|
||||||
|
new(ChatRole.User, prompt),
|
||||||
|
];
|
||||||
|
|
||||||
return $"LLM sampling result: {sampleResult.Content.Text}";
|
ChatOptions options = new()
|
||||||
}
|
|
||||||
|
|
||||||
private static CreateMessageRequestParams CreateRequestSamplingParams(string context, string uri, int maxTokens = 100)
|
|
||||||
{
|
|
||||||
return new CreateMessageRequestParams()
|
|
||||||
{
|
{
|
||||||
Messages = [new SamplingMessage()
|
MaxOutputTokens = maxTokens,
|
||||||
{
|
|
||||||
Role = Role.User,
|
|
||||||
Content = new Content()
|
|
||||||
{
|
|
||||||
Type = "text",
|
|
||||||
Text = $"Resource {uri} context: {context}"
|
|
||||||
}
|
|
||||||
}],
|
|
||||||
SystemPrompt = "You are a helpful test server.",
|
|
||||||
MaxTokens = maxTokens,
|
|
||||||
Temperature = 0.7f,
|
Temperature = 0.7f,
|
||||||
IncludeContext = ContextInclusion.ThisServer
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var samplingResponse = await thisServer.AsSamplingChatClient().GetResponseAsync(messages, options, cancellationToken);
|
||||||
|
|
||||||
|
return $"LLM sampling result: {samplingResponse}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@
|
||||||
<PackageReference Include="Microsoft.Extensions.AI" />
|
<PackageReference Include="Microsoft.Extensions.AI" />
|
||||||
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
|
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
|
||||||
<PackageReference Include="Anthropic.SDK" />
|
<PackageReference Include="Anthropic.SDK" />
|
||||||
<PackageReference Include="System.Linq.AsyncEnumerable" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ var mcpClient = await McpClientFactory.CreateAsync(
|
||||||
|
|
||||||
// Get all available tools
|
// Get all available tools
|
||||||
Console.WriteLine("Tools available:");
|
Console.WriteLine("Tools available:");
|
||||||
var tools = await mcpClient.GetAIFunctionsAsync();
|
var tools = await mcpClient.ListToolsAsync();
|
||||||
foreach (var tool in tools)
|
foreach (var tool in tools)
|
||||||
{
|
{
|
||||||
Console.WriteLine($" {tool}");
|
Console.WriteLine($" {tool}");
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ try
|
||||||
builder.Services.AddSerilog();
|
builder.Services.AddSerilog();
|
||||||
builder.Services.AddMcpServer()
|
builder.Services.AddMcpServer()
|
||||||
.WithStdioServerTransport()
|
.WithStdioServerTransport()
|
||||||
.WithTools();
|
.WithToolsFromAssembly();
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,10 @@ using System.ComponentModel;
|
||||||
|
|
||||||
namespace TestServerWithHosting.Tools;
|
namespace TestServerWithHosting.Tools;
|
||||||
|
|
||||||
[McpToolType]
|
[McpServerToolType]
|
||||||
public static class EchoTool
|
public static class EchoTool
|
||||||
{
|
{
|
||||||
[McpTool, Description("Echoes the input back to the client.")]
|
[McpServerTool, Description("Echoes the input back to the client.")]
|
||||||
public static string Echo(string message)
|
public static string Echo(string message)
|
||||||
{
|
{
|
||||||
return "hello " + message;
|
return "hello " + message;
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ namespace TestServerWithHosting.Tools;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// This tool uses depenency injection and async method
|
/// This tool uses depenency injection and async method
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[McpToolType]
|
[McpServerToolType]
|
||||||
public class SampleLlmTool
|
public class SampleLlmTool
|
||||||
{
|
{
|
||||||
private readonly IMcpServer _server;
|
private readonly IMcpServer _server;
|
||||||
|
|
@ -17,7 +17,7 @@ public class SampleLlmTool
|
||||||
_server = server ?? throw new ArgumentNullException(nameof(server));
|
_server = server ?? throw new ArgumentNullException(nameof(server));
|
||||||
}
|
}
|
||||||
|
|
||||||
[McpTool("sampleLLM"), Description("Samples from an LLM using MCP's sampling feature")]
|
[McpServerTool("sampleLLM"), Description("Samples from an LLM using MCP's sampling feature")]
|
||||||
public async Task<string> SampleLLM(
|
public async Task<string> SampleLLM(
|
||||||
[Description("The prompt to send to the LLM")] string prompt,
|
[Description("The prompt to send to the LLM")] string prompt,
|
||||||
[Description("Maximum number of tokens to generate")] int maxTokens,
|
[Description("Maximum number of tokens to generate")] int maxTokens,
|
||||||
|
|
|
||||||
|
|
@ -15,4 +15,7 @@ internal static class CollectionExtensions
|
||||||
|
|
||||||
return dictionary.TryGetValue(key, out TValue? value) ? value : defaultValue;
|
return dictionary.TryGetValue(key, out TValue? value) ? value : defaultValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Dictionary<TKey, TValue> ToDictionary<TKey, TValue>(this IEnumerable<KeyValuePair<TKey, TValue>> source) =>
|
||||||
|
source.ToDictionary(kv => kv.Key, kv => kv.Value);
|
||||||
}
|
}
|
||||||
104
src/ModelContextProtocol/AIContentExtensions.cs
Normal file
104
src/ModelContextProtocol/AIContentExtensions.cs
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
using Microsoft.Extensions.AI;
|
||||||
|
using ModelContextProtocol.Protocol.Types;
|
||||||
|
using ModelContextProtocol.Utils;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace ModelContextProtocol;
|
||||||
|
|
||||||
|
/// <summary>Provides helpers for conversions related to <see cref="AIContent"/>.</summary>
|
||||||
|
public static class AIContentExtensions
|
||||||
|
{
|
||||||
|
/// <summary>Creates a <see cref="ChatMessage"/> from a <see cref="PromptMessage"/>.</summary>
|
||||||
|
/// <param name="promptMessage">The message to convert.</param>
|
||||||
|
/// <returns>The created <see cref="ChatMessage"/>.</returns>
|
||||||
|
public static ChatMessage ToChatMessage(this PromptMessage promptMessage)
|
||||||
|
{
|
||||||
|
Throw.IfNull(promptMessage);
|
||||||
|
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
RawRepresentation = promptMessage,
|
||||||
|
Role = promptMessage.Role == Role.User ? ChatRole.User : ChatRole.Assistant,
|
||||||
|
Contents = [ToAIContent(promptMessage.Content)]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Creates a new <see cref="AIContent"/> from the content of a <see cref="Content"/>.</summary>
|
||||||
|
/// <param name="content">The <see cref="Content"/> to convert.</param>
|
||||||
|
/// <returns>The created <see cref="AIContent"/>.</returns>
|
||||||
|
public static AIContent ToAIContent(this Content content)
|
||||||
|
{
|
||||||
|
Throw.IfNull(content);
|
||||||
|
|
||||||
|
AIContent ac;
|
||||||
|
if (content is { Type: "image", MimeType: not null, Data: not null })
|
||||||
|
{
|
||||||
|
ac = new DataContent(Convert.FromBase64String(content.Data), content.MimeType);
|
||||||
|
}
|
||||||
|
else if (content is { Type: "resource" } && content.Resource is { } resourceContents)
|
||||||
|
{
|
||||||
|
ac = resourceContents.Blob is not null && resourceContents.MimeType is not null ?
|
||||||
|
new DataContent(Convert.FromBase64String(resourceContents.Blob), resourceContents.MimeType) :
|
||||||
|
new TextContent(resourceContents.Text);
|
||||||
|
|
||||||
|
(ac.AdditionalProperties ??= [])["uri"] = resourceContents.Uri;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ac = new TextContent(content.Text);
|
||||||
|
}
|
||||||
|
|
||||||
|
ac.RawRepresentation = content;
|
||||||
|
|
||||||
|
return ac;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Creates a new <see cref="AIContent"/> from the content of a <see cref="ResourceContents"/>.</summary>
|
||||||
|
/// <param name="content">The <see cref="ResourceContents"/> to convert.</param>
|
||||||
|
/// <returns>The created <see cref="AIContent"/>.</returns>
|
||||||
|
public static AIContent ToAIContent(this ResourceContents content)
|
||||||
|
{
|
||||||
|
Throw.IfNull(content);
|
||||||
|
|
||||||
|
AIContent ac = content.Blob is not null && content.MimeType is not null ?
|
||||||
|
new DataContent(Convert.FromBase64String(content.Blob), content.MimeType) :
|
||||||
|
new TextContent(content.Text);
|
||||||
|
|
||||||
|
(ac.AdditionalProperties ??= [])["uri"] = content.Uri;
|
||||||
|
ac.RawRepresentation = content;
|
||||||
|
|
||||||
|
return ac;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Creates a list of <see cref="AIContent"/> from a sequence of <see cref="Content"/>.</summary>
|
||||||
|
/// <param name="contents">The <see cref="Content"/> instances to convert.</param>
|
||||||
|
/// <returns>The created <see cref="AIContent"/> instances.</returns>
|
||||||
|
public static IList<AIContent> ToAIContents(this IEnumerable<Content> contents)
|
||||||
|
{
|
||||||
|
Throw.IfNull(contents);
|
||||||
|
|
||||||
|
return contents.Select(ToAIContent).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Creates a list of <see cref="AIContent"/> from a sequence of <see cref="ResourceContents"/>.</summary>
|
||||||
|
/// <param name="contents">The <see cref="ResourceContents"/> instances to convert.</param>
|
||||||
|
/// <returns>The created <see cref="AIContent"/> instances.</returns>
|
||||||
|
public static IList<AIContent> ToAIContents(this IEnumerable<ResourceContents> contents)
|
||||||
|
{
|
||||||
|
Throw.IfNull(contents);
|
||||||
|
|
||||||
|
return contents.Select(ToAIContent).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Extracts the data from a <see cref="DataContent"/> as a Base64 string.</summary>
|
||||||
|
internal static string GetBase64Data(this DataContent dataContent)
|
||||||
|
{
|
||||||
|
#if NET
|
||||||
|
return Convert.ToBase64String(dataContent.Data.Span);
|
||||||
|
#else
|
||||||
|
return MemoryMarshal.TryGetArray(dataContent.Data, out ArraySegment<byte> segment) ?
|
||||||
|
Convert.ToBase64String(segment.Array!, segment.Offset, segment.Count) :
|
||||||
|
Convert.ToBase64String(dataContent.Data.ToArray());
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,8 +3,8 @@ using ModelContextProtocol.Protocol.Types;
|
||||||
using ModelContextProtocol.Utils;
|
using ModelContextProtocol.Utils;
|
||||||
using ModelContextProtocol.Utils.Json;
|
using ModelContextProtocol.Utils.Json;
|
||||||
using Microsoft.Extensions.AI;
|
using Microsoft.Extensions.AI;
|
||||||
using System.Runtime.CompilerServices;
|
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
namespace ModelContextProtocol.Client;
|
namespace ModelContextProtocol.Client;
|
||||||
|
|
||||||
|
|
@ -23,6 +23,7 @@ public static class McpClientExtensions
|
||||||
public static Task SendNotificationAsync(this IMcpClient client, string method, object? parameters = null, CancellationToken cancellationToken = default)
|
public static Task SendNotificationAsync(this IMcpClient client, string method, object? parameters = null, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
Throw.IfNull(client);
|
Throw.IfNull(client);
|
||||||
|
Throw.IfNullOrWhiteSpace(method);
|
||||||
|
|
||||||
return client.SendMessageAsync(
|
return client.SendMessageAsync(
|
||||||
new JsonRpcNotification { Method = method, Params = parameters },
|
new JsonRpcNotification { Method = method, Params = parameters },
|
||||||
|
|
@ -45,42 +46,67 @@ public static class McpClientExtensions
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Retrieves a sequence of available tools from the server.
|
/// Retrieves a list of available tools from the server.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="client">The client.</param>
|
/// <param name="client">The client.</param>
|
||||||
/// <param name="cancellationToken">A token to cancel the operation.</param>
|
/// <param name="cancellationToken">A token to cancel the operation.</param>
|
||||||
/// <returns>An asynchronous sequence of tool information.</returns>
|
/// <returns>A list of all available tools.</returns>
|
||||||
public static async IAsyncEnumerable<Tool> ListToolsAsync(
|
public static async Task<IList<McpClientTool>> ListToolsAsync(
|
||||||
this IMcpClient client, [EnumeratorCancellation] CancellationToken cancellationToken = default)
|
this IMcpClient client, CancellationToken cancellationToken = default)
|
||||||
{
|
|
||||||
string? cursor = null;
|
|
||||||
do
|
|
||||||
{
|
|
||||||
var tools = await ListToolsAsync(client, cursor, cancellationToken).ConfigureAwait(false);
|
|
||||||
foreach (var tool in tools.Tools)
|
|
||||||
{
|
|
||||||
yield return tool;
|
|
||||||
}
|
|
||||||
|
|
||||||
cursor = tools.NextCursor;
|
|
||||||
}
|
|
||||||
while (cursor is not null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Retrieves a sequence of available tools from the server.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="client">The client.</param>
|
|
||||||
/// <param name="cursor">A cursor to paginate the results.</param>
|
|
||||||
/// <param name="cancellationToken">A token to cancel the operation.</param>
|
|
||||||
/// <returns>A task containing the server's response with tool information.</returns>
|
|
||||||
public static Task<ListToolsResult> ListToolsAsync(this IMcpClient client, string? cursor, CancellationToken cancellationToken = default)
|
|
||||||
{
|
{
|
||||||
Throw.IfNull(client);
|
Throw.IfNull(client);
|
||||||
|
|
||||||
return client.SendRequestAsync<ListToolsResult>(
|
List<McpClientTool>? tools = null;
|
||||||
CreateRequest("tools/list", CreateCursorDictionary(cursor)),
|
string? cursor = null;
|
||||||
cancellationToken);
|
do
|
||||||
|
{
|
||||||
|
var toolResults = await client.SendRequestAsync<ListToolsResult>(
|
||||||
|
CreateRequest("tools/list", CreateCursorDictionary(cursor)),
|
||||||
|
cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
tools ??= new List<McpClientTool>(toolResults.Tools.Count);
|
||||||
|
foreach (var tool in toolResults.Tools)
|
||||||
|
{
|
||||||
|
tools.Add(new McpClientTool(client, tool));
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor = toolResults.NextCursor;
|
||||||
|
}
|
||||||
|
while (cursor is not null);
|
||||||
|
|
||||||
|
return tools;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates an enumerable for asynchronously enumerating all available tools from the server.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="client">The client.</param>
|
||||||
|
/// <param name="cancellationToken">A token to cancel the operation.</param>
|
||||||
|
/// <returns>An asynchronous sequence of all available tools.</returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// Every iteration through the returned <see cref="IAsyncEnumerable{McpClientTool}"/>
|
||||||
|
/// will result in requerying the server and yielding the sequence of available tools.
|
||||||
|
/// </remarks>
|
||||||
|
public static async IAsyncEnumerable<McpClientTool> EnumerateToolsAsync(
|
||||||
|
this IMcpClient client, [EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
Throw.IfNull(client);
|
||||||
|
|
||||||
|
string? cursor = null;
|
||||||
|
do
|
||||||
|
{
|
||||||
|
var toolResults = await client.SendRequestAsync<ListToolsResult>(
|
||||||
|
CreateRequest("tools/list", CreateCursorDictionary(cursor)),
|
||||||
|
cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
foreach (var tool in toolResults.Tools)
|
||||||
|
{
|
||||||
|
yield return new McpClientTool(client, tool);
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor = toolResults.NextCursor;
|
||||||
|
}
|
||||||
|
while (cursor is not null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -88,40 +114,69 @@ public static class McpClientExtensions
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="client">The client.</param>
|
/// <param name="client">The client.</param>
|
||||||
/// <param name="cancellationToken">A token to cancel the operation.</param>
|
/// <param name="cancellationToken">A token to cancel the operation.</param>
|
||||||
/// <returns>An asynchronous sequence of prompt information.</returns>
|
/// <returns>A list of all available prompts.</returns>
|
||||||
public static async IAsyncEnumerable<Prompt> ListPromptsAsync(
|
public static async Task<IList<Prompt>> ListPromptsAsync(
|
||||||
this IMcpClient client, [EnumeratorCancellation] CancellationToken cancellationToken = default)
|
this IMcpClient client, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
Throw.IfNull(client);
|
||||||
|
|
||||||
|
List<Prompt>? prompts = null;
|
||||||
|
|
||||||
string? cursor = null;
|
string? cursor = null;
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
var prompts = await ListPromptsAsync(client, cursor, cancellationToken).ConfigureAwait(false);
|
var promptResults = await client.SendRequestAsync<ListPromptsResult>(
|
||||||
foreach (var prompt in prompts.Prompts)
|
CreateRequest("prompts/list", CreateCursorDictionary(cursor)),
|
||||||
|
cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (prompts is null)
|
||||||
|
{
|
||||||
|
prompts = promptResults.Prompts;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
prompts.AddRange(promptResults.Prompts);
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor = promptResults.NextCursor;
|
||||||
|
}
|
||||||
|
while (cursor is not null);
|
||||||
|
|
||||||
|
return prompts;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates an enumerable for asynchronously enumerating all available prompts from the server.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="client">The client.</param>
|
||||||
|
/// <param name="cancellationToken">A token to cancel the operation.</param>
|
||||||
|
/// <returns>An asynchronous sequence of all available prompts.</returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// Every iteration through the returned <see cref="IAsyncEnumerable{Prompt}"/>
|
||||||
|
/// will result in requerying the server and yielding the sequence of available prompts.
|
||||||
|
/// </remarks>
|
||||||
|
public static async IAsyncEnumerable<Prompt> EnumeratePromptsAsync(
|
||||||
|
this IMcpClient client, [EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
Throw.IfNull(client);
|
||||||
|
|
||||||
|
string? cursor = null;
|
||||||
|
do
|
||||||
|
{
|
||||||
|
var promptResults = await client.SendRequestAsync<ListPromptsResult>(
|
||||||
|
CreateRequest("prompts/list", CreateCursorDictionary(cursor)),
|
||||||
|
cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
foreach (var prompt in promptResults.Prompts)
|
||||||
{
|
{
|
||||||
yield return prompt;
|
yield return prompt;
|
||||||
}
|
}
|
||||||
|
|
||||||
cursor = prompts.NextCursor;
|
cursor = promptResults.NextCursor;
|
||||||
}
|
}
|
||||||
while (cursor is not null);
|
while (cursor is not null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Retrieves a list of available prompts from the server.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="client">The client.</param>
|
|
||||||
/// <param name="cursor">A cursor to paginate the results.</param>
|
|
||||||
/// <param name="cancellationToken">A token to cancel the operation.</param>
|
|
||||||
/// <returns>A task containing the server's response with prompt information.</returns>
|
|
||||||
public static Task<ListPromptsResult> ListPromptsAsync(this IMcpClient client, string? cursor, CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
Throw.IfNull(client);
|
|
||||||
|
|
||||||
return client.SendRequestAsync<ListPromptsResult>(
|
|
||||||
CreateRequest("prompts/list", CreateCursorDictionary(cursor)),
|
|
||||||
cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Retrieves a specific prompt with optional arguments.
|
/// Retrieves a specific prompt with optional arguments.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -130,9 +185,11 @@ public static class McpClientExtensions
|
||||||
/// <param name="arguments">Optional arguments for the prompt</param>
|
/// <param name="arguments">Optional arguments for the prompt</param>
|
||||||
/// <param name="cancellationToken">A token to cancel the operation.</param>
|
/// <param name="cancellationToken">A token to cancel the operation.</param>
|
||||||
/// <returns>A task containing the prompt's content and messages.</returns>
|
/// <returns>A task containing the prompt's content and messages.</returns>
|
||||||
public static Task<GetPromptResult> GetPromptAsync(this IMcpClient client, string name, Dictionary<string, object>? arguments = null, CancellationToken cancellationToken = default)
|
public static Task<GetPromptResult> GetPromptAsync(
|
||||||
|
this IMcpClient client, string name, Dictionary<string, object?>? arguments = null, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
Throw.IfNull(client);
|
Throw.IfNull(client);
|
||||||
|
Throw.IfNullOrWhiteSpace(name);
|
||||||
|
|
||||||
return client.SendRequestAsync<GetPromptResult>(
|
return client.SendRequestAsync<GetPromptResult>(
|
||||||
CreateRequest("prompts/get", CreateParametersDictionary(name, arguments)),
|
CreateRequest("prompts/get", CreateParametersDictionary(name, arguments)),
|
||||||
|
|
@ -140,24 +197,69 @@ public static class McpClientExtensions
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Retrieves a sequence of available resource templates from the server.
|
/// Retrieves a list of available resource templates from the server.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="client">The client.</param>
|
/// <param name="client">The client.</param>
|
||||||
/// <param name="cancellationToken">A token to cancel the operation.</param>
|
/// <param name="cancellationToken">A token to cancel the operation.</param>
|
||||||
/// <returns>An asynchronous sequence of resource template information.</returns>
|
/// <returns>A list of all available resource templates.</returns>
|
||||||
public static async IAsyncEnumerable<ResourceTemplate> ListResourceTemplatesAsync(
|
public static async Task<IList<ResourceTemplate>> ListResourceTemplatesAsync(
|
||||||
this IMcpClient client, [EnumeratorCancellation] CancellationToken cancellationToken = default)
|
this IMcpClient client, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
Throw.IfNull(client);
|
||||||
|
|
||||||
|
List<ResourceTemplate>? templates = null;
|
||||||
|
|
||||||
string? cursor = null;
|
string? cursor = null;
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
var resources = await ListResourceTemplatesAsync(client, cursor, cancellationToken).ConfigureAwait(false);
|
var templateResults = await client.SendRequestAsync<ListResourceTemplatesResult>(
|
||||||
foreach (var resource in resources.ResourceTemplates)
|
CreateRequest("resources/templates/list", CreateCursorDictionary(cursor)),
|
||||||
|
cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (templates is null)
|
||||||
{
|
{
|
||||||
yield return resource;
|
templates = templateResults.ResourceTemplates;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
templates.AddRange(templateResults.ResourceTemplates);
|
||||||
}
|
}
|
||||||
|
|
||||||
cursor = resources.NextCursor;
|
cursor = templateResults.NextCursor;
|
||||||
|
}
|
||||||
|
while (cursor is not null);
|
||||||
|
|
||||||
|
return templates;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates an enumerable for asynchronously enumerating all available resource templates from the server.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="client">The client.</param>
|
||||||
|
/// <param name="cancellationToken">A token to cancel the operation.</param>
|
||||||
|
/// <returns>An asynchronous sequence of all available resource templates.</returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// Every iteration through the returned <see cref="IAsyncEnumerable{ResourceTemplate}"/>
|
||||||
|
/// will result in requerying the server and yielding the sequence of available resource templates.
|
||||||
|
/// </remarks>
|
||||||
|
public static async IAsyncEnumerable<ResourceTemplate> EnumerateResourceTemplatesAsync(
|
||||||
|
this IMcpClient client, [EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
Throw.IfNull(client);
|
||||||
|
|
||||||
|
string? cursor = null;
|
||||||
|
do
|
||||||
|
{
|
||||||
|
var templateResults = await client.SendRequestAsync<ListResourceTemplatesResult>(
|
||||||
|
CreateRequest("resources/templates/list", CreateCursorDictionary(cursor)),
|
||||||
|
cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
foreach (var template in templateResults.ResourceTemplates)
|
||||||
|
{
|
||||||
|
yield return template;
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor = templateResults.NextCursor;
|
||||||
}
|
}
|
||||||
while (cursor is not null);
|
while (cursor is not null);
|
||||||
}
|
}
|
||||||
|
|
@ -166,64 +268,81 @@ public static class McpClientExtensions
|
||||||
/// Retrieves a list of available resources from the server.
|
/// Retrieves a list of available resources from the server.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="client">The client.</param>
|
/// <param name="client">The client.</param>
|
||||||
/// <param name="cursor">A cursor to paginate the results.</param>
|
|
||||||
/// <param name="cancellationToken">A token to cancel the operation.</param>
|
/// <param name="cancellationToken">A token to cancel the operation.</param>
|
||||||
public static Task<ListResourceTemplatesResult> ListResourceTemplatesAsync(this IMcpClient client, string? cursor, CancellationToken cancellationToken = default)
|
/// <returns>A list of all available resources.</returns>
|
||||||
|
public static async Task<IList<Resource>> ListResourcesAsync(
|
||||||
|
this IMcpClient client, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
Throw.IfNull(client);
|
Throw.IfNull(client);
|
||||||
|
|
||||||
return client.SendRequestAsync<ListResourceTemplatesResult>(
|
List<Resource>? resources = null;
|
||||||
CreateRequest("resources/templates/list", CreateCursorDictionary(cursor)),
|
|
||||||
cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Retrieves a sequence of available resources from the server.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="client">The client.</param>
|
|
||||||
/// <param name="cancellationToken">A token to cancel the operation.</param>
|
|
||||||
/// <returns>An asynchronous sequence of resource information.</returns>
|
|
||||||
public static async IAsyncEnumerable<Resource> ListResourcesAsync(
|
|
||||||
this IMcpClient client, [EnumeratorCancellation] CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
string? cursor = null;
|
string? cursor = null;
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
var resources = await ListResourcesAsync(client, cursor, cancellationToken).ConfigureAwait(false);
|
var resourceResults = await client.SendRequestAsync<ListResourcesResult>(
|
||||||
foreach (var resource in resources.Resources)
|
CreateRequest("resources/list", CreateCursorDictionary(cursor)),
|
||||||
|
cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (resources is null)
|
||||||
|
{
|
||||||
|
resources = resourceResults.Resources;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
resources.AddRange(resourceResults.Resources);
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor = resourceResults.NextCursor;
|
||||||
|
}
|
||||||
|
while (cursor is not null);
|
||||||
|
|
||||||
|
return resources;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates an enumerable for asynchronously enumerating all available resources from the server.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="client">The client.</param>
|
||||||
|
/// <param name="cancellationToken">A token to cancel the operation.</param>
|
||||||
|
/// <returns>An asynchronous sequence of all available resources.</returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// Every iteration through the returned <see cref="IAsyncEnumerable{Resource}"/>
|
||||||
|
/// will result in requerying the server and yielding the sequence of available resources.
|
||||||
|
/// </remarks>
|
||||||
|
public static async IAsyncEnumerable<Resource> EnumerateResourcesAsync(
|
||||||
|
this IMcpClient client, [EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
Throw.IfNull(client);
|
||||||
|
|
||||||
|
string? cursor = null;
|
||||||
|
do
|
||||||
|
{
|
||||||
|
var resourceResults = await client.SendRequestAsync<ListResourcesResult>(
|
||||||
|
CreateRequest("resources/list", CreateCursorDictionary(cursor)),
|
||||||
|
cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
foreach (var resource in resourceResults.Resources)
|
||||||
{
|
{
|
||||||
yield return resource;
|
yield return resource;
|
||||||
}
|
}
|
||||||
|
|
||||||
cursor = resources.NextCursor;
|
cursor = resourceResults.NextCursor;
|
||||||
}
|
}
|
||||||
while (cursor is not null);
|
while (cursor is not null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Retrieves a list of available resources from the server.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="client">The client.</param>
|
|
||||||
/// <param name="cursor">A cursor to paginate the results.</param>
|
|
||||||
/// <param name="cancellationToken">A token to cancel the operation.</param>
|
|
||||||
public static Task<ListResourcesResult> ListResourcesAsync(this IMcpClient client, string? cursor, CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
Throw.IfNull(client);
|
|
||||||
|
|
||||||
return client.SendRequestAsync<ListResourcesResult>(
|
|
||||||
CreateRequest("resources/list", CreateCursorDictionary(cursor)),
|
|
||||||
cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Reads a resource from the server.
|
/// Reads a resource from the server.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="client">The client.</param>
|
/// <param name="client">The client.</param>
|
||||||
/// <param name="uri">The uri of the resource.</param>
|
/// <param name="uri">The uri of the resource.</param>
|
||||||
/// <param name="cancellationToken">A token to cancel the operation.</param>
|
/// <param name="cancellationToken">A token to cancel the operation.</param>
|
||||||
public static Task<ReadResourceResult> ReadResourceAsync(this IMcpClient client, string uri, CancellationToken cancellationToken = default)
|
public static Task<ReadResourceResult> ReadResourceAsync(
|
||||||
|
this IMcpClient client, string uri, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
Throw.IfNull(client);
|
Throw.IfNull(client);
|
||||||
|
Throw.IfNullOrWhiteSpace(uri);
|
||||||
|
|
||||||
return client.SendRequestAsync<ReadResourceResult>(
|
return client.SendRequestAsync<ReadResourceResult>(
|
||||||
CreateRequest("resources/read", new() { ["uri"] = uri }),
|
CreateRequest("resources/read", new() { ["uri"] = uri }),
|
||||||
|
|
@ -267,6 +386,7 @@ public static class McpClientExtensions
|
||||||
public static Task SubscribeToResourceAsync(this IMcpClient client, string uri, CancellationToken cancellationToken = default)
|
public static Task SubscribeToResourceAsync(this IMcpClient client, string uri, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
Throw.IfNull(client);
|
Throw.IfNull(client);
|
||||||
|
Throw.IfNullOrWhiteSpace(uri);
|
||||||
|
|
||||||
return client.SendRequestAsync<EmptyResult>(
|
return client.SendRequestAsync<EmptyResult>(
|
||||||
CreateRequest("resources/subscribe", new() { ["uri"] = uri }),
|
CreateRequest("resources/subscribe", new() { ["uri"] = uri }),
|
||||||
|
|
@ -282,6 +402,7 @@ public static class McpClientExtensions
|
||||||
public static Task UnsubscribeFromResourceAsync(this IMcpClient client, string uri, CancellationToken cancellationToken = default)
|
public static Task UnsubscribeFromResourceAsync(this IMcpClient client, string uri, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
Throw.IfNull(client);
|
Throw.IfNull(client);
|
||||||
|
Throw.IfNullOrWhiteSpace(uri);
|
||||||
|
|
||||||
return client.SendRequestAsync<EmptyResult>(
|
return client.SendRequestAsync<EmptyResult>(
|
||||||
CreateRequest("resources/unsubscribe", new() { ["uri"] = uri }),
|
CreateRequest("resources/unsubscribe", new() { ["uri"] = uri }),
|
||||||
|
|
@ -296,48 +417,17 @@ public static class McpClientExtensions
|
||||||
/// <param name="arguments">Optional arguments for the tool.</param>
|
/// <param name="arguments">Optional arguments for the tool.</param>
|
||||||
/// <param name="cancellationToken">A token to cancel the operation.</param>
|
/// <param name="cancellationToken">A token to cancel the operation.</param>
|
||||||
/// <returns>A task containing the tool's response.</returns>
|
/// <returns>A task containing the tool's response.</returns>
|
||||||
public static Task<CallToolResponse> CallToolAsync(this IMcpClient client, string toolName, Dictionary<string, object> arguments, CancellationToken cancellationToken = default)
|
public static Task<CallToolResponse> CallToolAsync(
|
||||||
|
this IMcpClient client, string toolName, IReadOnlyDictionary<string, object?>? arguments = null, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
Throw.IfNull(client);
|
Throw.IfNull(client);
|
||||||
|
Throw.IfNull(toolName);
|
||||||
|
|
||||||
return client.SendRequestAsync<CallToolResponse>(
|
return client.SendRequestAsync<CallToolResponse>(
|
||||||
CreateRequest("tools/call", CreateParametersDictionary(toolName, arguments)),
|
CreateRequest("tools/call", CreateParametersDictionary(toolName, arguments)),
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Gets <see cref="AIFunction"/> instances for all of the tools available through the specified <see cref="IMcpClient"/>.</summary>
|
|
||||||
/// <param name="client">The client for which <see cref="AIFunction"/> instances should be created.</param>
|
|
||||||
/// <param name="cancellationToken">A token to cancel the operation.</param>
|
|
||||||
/// <returns>A task containing a list of the available functions.</returns>
|
|
||||||
public static async Task<IList<AIFunction>> GetAIFunctionsAsync(this IMcpClient client, CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
Throw.IfNull(client);
|
|
||||||
|
|
||||||
List<AIFunction> functions = [];
|
|
||||||
await foreach (var tool in client.ListToolsAsync(cancellationToken).ConfigureAwait(false))
|
|
||||||
{
|
|
||||||
functions.Add(AsAIFunction(client, tool));
|
|
||||||
}
|
|
||||||
|
|
||||||
return functions;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Gets an <see cref="AIFunction"/> for invoking <see cref="Tool"/> via this <see cref="IMcpClient"/>.</summary>
|
|
||||||
/// <param name="client">The client with which to perform the invocation.</param>
|
|
||||||
/// <param name="tool">The tool to be invoked.</param>
|
|
||||||
/// <returns>An <see cref="AIFunction"/> for performing the call.</returns>
|
|
||||||
/// <remarks>
|
|
||||||
/// This operation does not validate that <paramref name="tool"/> is valid for the specified <paramref name="client"/>.
|
|
||||||
/// If the tool is not valid for the client, it will fail when invoked.
|
|
||||||
/// </remarks>
|
|
||||||
public static AIFunction AsAIFunction(this IMcpClient client, Tool tool)
|
|
||||||
{
|
|
||||||
Throw.IfNull(client);
|
|
||||||
Throw.IfNull(tool);
|
|
||||||
|
|
||||||
return new McpAIFunction(client, tool);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Converts the contents of a <see cref="CreateMessageRequestParams"/> into a pair of
|
/// Converts the contents of a <see cref="CreateMessageRequestParams"/> into a pair of
|
||||||
/// <see cref="IEnumerable{ChatMessage}"/> and <see cref="ChatOptions"/> instances to use
|
/// <see cref="IEnumerable{ChatMessage}"/> and <see cref="ChatOptions"/> instances to use
|
||||||
|
|
@ -428,12 +518,7 @@ public static class McpClientExtensions
|
||||||
{
|
{
|
||||||
Type = "image",
|
Type = "image",
|
||||||
MimeType = dc.MediaType,
|
MimeType = dc.MediaType,
|
||||||
Data = Convert.ToBase64String(dc.Data
|
Data = dc.GetBase64Data(),
|
||||||
#if NET
|
|
||||||
.Span),
|
|
||||||
#else
|
|
||||||
.ToArray()),
|
|
||||||
#endif
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -499,7 +584,8 @@ public static class McpClientExtensions
|
||||||
private static Dictionary<string, object?>? CreateCursorDictionary(string? cursor) =>
|
private static Dictionary<string, object?>? CreateCursorDictionary(string? cursor) =>
|
||||||
cursor != null ? new() { ["cursor"] = cursor } : null;
|
cursor != null ? new() { ["cursor"] = cursor } : null;
|
||||||
|
|
||||||
private static Dictionary<string, object?> CreateParametersDictionary(string nameParameter, Dictionary<string, object>? arguments)
|
private static Dictionary<string, object?> CreateParametersDictionary(
|
||||||
|
string nameParameter, IReadOnlyDictionary<string, object?>? arguments)
|
||||||
{
|
{
|
||||||
Dictionary<string, object?> parameters = new()
|
Dictionary<string, object?> parameters = new()
|
||||||
{
|
{
|
||||||
|
|
@ -526,20 +612,20 @@ public static class McpClientExtensions
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public override JsonElement JsonSchema => tool.InputSchema;
|
public override JsonElement JsonSchema => tool.InputSchema;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override JsonSerializerOptions JsonSerializerOptions => McpJsonUtilities.DefaultOptions;
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
protected async override Task<object?> InvokeCoreAsync(
|
protected async override Task<object?> InvokeCoreAsync(
|
||||||
IEnumerable<KeyValuePair<string, object?>> arguments, CancellationToken cancellationToken)
|
IEnumerable<KeyValuePair<string, object?>> arguments, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
Throw.IfNull(arguments);
|
IReadOnlyDictionary<string, object?> argDict =
|
||||||
|
arguments as IReadOnlyDictionary<string, object?> ??
|
||||||
Dictionary<string, object> argDict = [];
|
#if NET
|
||||||
foreach (var arg in arguments)
|
arguments.ToDictionary();
|
||||||
{
|
#else
|
||||||
if (arg.Value is not null)
|
arguments.ToDictionary(kv => kv.Key, kv => kv.Value);
|
||||||
{
|
#endif
|
||||||
argDict[arg.Key] = arg.Value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CallToolResponse result = await client.CallToolAsync(tool.Name, argDict, cancellationToken).ConfigureAwait(false);
|
CallToolResponse result = await client.CallToolAsync(tool.Name, argDict, cancellationToken).ConfigureAwait(false);
|
||||||
return JsonSerializer.SerializeToElement(result, McpJsonUtilities.JsonContext.Default.CallToolResponse);
|
return JsonSerializer.SerializeToElement(result, McpJsonUtilities.JsonContext.Default.CallToolResponse);
|
||||||
|
|
|
||||||
43
src/ModelContextProtocol/Client/McpClientTool.cs
Normal file
43
src/ModelContextProtocol/Client/McpClientTool.cs
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
using ModelContextProtocol.Protocol.Types;
|
||||||
|
using ModelContextProtocol.Utils.Json;
|
||||||
|
using Microsoft.Extensions.AI;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace ModelContextProtocol.Client;
|
||||||
|
|
||||||
|
/// <summary>Provides an AI function that calls a tool through <see cref="IMcpClient"/>.</summary>
|
||||||
|
public sealed class McpClientTool : AIFunction
|
||||||
|
{
|
||||||
|
private readonly IMcpClient _client;
|
||||||
|
private readonly Tool _tool;
|
||||||
|
|
||||||
|
internal McpClientTool(IMcpClient client, Tool tool)
|
||||||
|
{
|
||||||
|
_client = client;
|
||||||
|
_tool = tool;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override string Name => _tool.Name;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override string Description => _tool.Description ?? string.Empty;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override JsonElement JsonSchema => _tool.InputSchema;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override JsonSerializerOptions JsonSerializerOptions => McpJsonUtilities.DefaultOptions;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
protected async override Task<object?> InvokeCoreAsync(
|
||||||
|
IEnumerable<KeyValuePair<string, object?>> arguments, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
IReadOnlyDictionary<string, object?> argDict =
|
||||||
|
arguments as IReadOnlyDictionary<string, object?> ??
|
||||||
|
arguments.ToDictionary();
|
||||||
|
|
||||||
|
CallToolResponse result = await _client.CallToolAsync(_tool.Name, argDict, cancellationToken).ConfigureAwait(false);
|
||||||
|
return JsonSerializer.SerializeToElement(result, McpJsonUtilities.JsonContext.Default.CallToolResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,12 +1,9 @@
|
||||||
using ModelContextProtocol.Configuration;
|
using ModelContextProtocol.Configuration;
|
||||||
using ModelContextProtocol.Protocol.Types;
|
|
||||||
using ModelContextProtocol.Server;
|
using ModelContextProtocol.Server;
|
||||||
using ModelContextProtocol.Utils;
|
using ModelContextProtocol.Utils;
|
||||||
using ModelContextProtocol.Utils.Json;
|
|
||||||
using Microsoft.Extensions.AI;
|
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Text.Json;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
namespace ModelContextProtocol;
|
namespace ModelContextProtocol;
|
||||||
|
|
||||||
|
|
@ -23,23 +20,17 @@ public static partial class McpServerBuilderExtensions
|
||||||
/// <typeparam name="TTool">The tool type.</typeparam>
|
/// <typeparam name="TTool">The tool type.</typeparam>
|
||||||
/// <param name="builder">The builder instance.</param>
|
/// <param name="builder">The builder instance.</param>
|
||||||
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception>
|
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception>
|
||||||
public static IMcpServerBuilder WithTool<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] TTool>(this IMcpServerBuilder builder)
|
public static IMcpServerBuilder WithTools<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods)] TTool>(
|
||||||
|
this IMcpServerBuilder builder)
|
||||||
{
|
{
|
||||||
Throw.IfNull(builder);
|
Throw.IfNull(builder);
|
||||||
List<AIFunction> functions = [];
|
|
||||||
|
|
||||||
PopulateFunctions(typeof(TTool), functions);
|
foreach (var toolMethod in GetToolMethods(typeof(TTool)))
|
||||||
return WithTools(builder, functions);
|
{
|
||||||
}
|
builder.Services.AddSingleton(services => McpServerTool.Create(toolMethod, services: services));
|
||||||
/// <summary>
|
}
|
||||||
/// Adds all tools marked with <see cref="McpToolTypeAttribute"/> from the current assembly to the server.
|
|
||||||
/// </summary>
|
return builder;
|
||||||
/// <param name="builder">The builder instance.</param>
|
|
||||||
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception>
|
|
||||||
[RequiresUnreferencedCode(RequiresUnreferencedCodeMessage)]
|
|
||||||
public static IMcpServerBuilder WithTools(this IMcpServerBuilder builder)
|
|
||||||
{
|
|
||||||
return WithToolsFromAssembly(builder, Assembly.GetCallingAssembly());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -55,147 +46,42 @@ public static partial class McpServerBuilderExtensions
|
||||||
Throw.IfNull(builder);
|
Throw.IfNull(builder);
|
||||||
Throw.IfNull(toolTypes);
|
Throw.IfNull(toolTypes);
|
||||||
|
|
||||||
List<AIFunction> functions = [];
|
|
||||||
|
|
||||||
foreach (var toolType in toolTypes)
|
foreach (var toolType in toolTypes)
|
||||||
{
|
{
|
||||||
if (toolType is null)
|
if (toolType is not null)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(toolTypes), $"A tool type provided by the enumerator was null.");
|
foreach (var toolMethod in GetToolMethods(toolType))
|
||||||
|
{
|
||||||
|
builder.Services.AddSingleton(services => McpServerTool.Create(toolMethod, services: services));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
PopulateFunctions(toolType, functions);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return WithTools(builder, functions);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Adds tools to the server.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="builder">The builder instance.</param>
|
|
||||||
/// <param name="functions"><see cref="AIFunction"/> instances to use as the tools.</param>
|
|
||||||
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception>
|
|
||||||
/// <exception cref="ArgumentNullException"><paramref name="functions"/> is <see langword="null"/>.</exception>
|
|
||||||
public static IMcpServerBuilder WithTools(this IMcpServerBuilder builder, params IEnumerable<AIFunction> functions)
|
|
||||||
{
|
|
||||||
Throw.IfNull(builder);
|
|
||||||
Throw.IfNull(functions);
|
|
||||||
|
|
||||||
List<Tool> tools = [];
|
|
||||||
Dictionary<string, Func<RequestContext<CallToolRequestParams>, CancellationToken, Task<CallToolResponse>>> callbacks = [];
|
|
||||||
|
|
||||||
foreach (AIFunction function in functions)
|
|
||||||
{
|
|
||||||
if (function is null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(functions), $"A function provided by the enumerator was null.");
|
|
||||||
}
|
|
||||||
|
|
||||||
tools.Add(new()
|
|
||||||
{
|
|
||||||
Name = function.Name,
|
|
||||||
Description = function.Description,
|
|
||||||
InputSchema = function.JsonSchema,
|
|
||||||
});
|
|
||||||
|
|
||||||
callbacks.Add(function.Name, async (request, cancellationToken) =>
|
|
||||||
{
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
|
||||||
|
|
||||||
object? result;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
result = await function.InvokeAsync((request.Params?.Arguments ?? [])!, cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
catch (Exception e) when (e is not OperationCanceledException)
|
|
||||||
{
|
|
||||||
return new CallToolResponse()
|
|
||||||
{
|
|
||||||
IsError = true,
|
|
||||||
Content = [new() { Text = e.Message, Type = "text" }],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (result)
|
|
||||||
{
|
|
||||||
case JsonElement je when je.ValueKind == JsonValueKind.Null:
|
|
||||||
return new() { Content = [] };
|
|
||||||
|
|
||||||
case JsonElement je when je.ValueKind == JsonValueKind.Array:
|
|
||||||
return new() { Content = je.EnumerateArray().Select(x => new Content() { Text = x.ToString(), Type = "text" }).ToList() };
|
|
||||||
|
|
||||||
default:
|
|
||||||
return new() { Content = [new() { Text = result?.ToString(), Type = "text" }] };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.WithListToolsHandler((_, _) => Task.FromResult(new ListToolsResult() { Tools = tools }));
|
|
||||||
|
|
||||||
builder.WithCallToolHandler(async (request, cancellationToken) =>
|
|
||||||
{
|
|
||||||
if (request.Params is null || !callbacks.TryGetValue(request.Params.Name, out var callback))
|
|
||||||
{
|
|
||||||
throw new McpServerException($"Unknown tool '{request.Params?.Name}'");
|
|
||||||
}
|
|
||||||
|
|
||||||
return await callback(request, cancellationToken).ConfigureAwait(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
return builder;
|
return builder;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Adds types marked with the <see cref="McpToolTypeAttribute"/> attribute from the given assembly as tools to the server.
|
/// Adds types marked with the <see cref="McpServerToolTypeAttribute"/> attribute from the given assembly as tools to the server.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="builder">The builder instance.</param>
|
/// <param name="builder">The builder instance.</param>
|
||||||
/// <param name="assembly">The assembly to load the types from. Null to get the current assembly</param>
|
/// <param name="toolAssembly">The assembly to load the types from. Null to get the current assembly</param>
|
||||||
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception>
|
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception>
|
||||||
[RequiresUnreferencedCode(RequiresUnreferencedCodeMessage)]
|
[RequiresUnreferencedCode(RequiresUnreferencedCodeMessage)]
|
||||||
public static IMcpServerBuilder WithToolsFromAssembly(this IMcpServerBuilder builder, Assembly? assembly = null)
|
public static IMcpServerBuilder WithToolsFromAssembly(this IMcpServerBuilder builder, Assembly? toolAssembly = null)
|
||||||
{
|
{
|
||||||
assembly ??= Assembly.GetCallingAssembly();
|
Throw.IfNull(builder);
|
||||||
|
|
||||||
List<Type> toolTypes = [];
|
toolAssembly ??= Assembly.GetCallingAssembly();
|
||||||
|
|
||||||
foreach (var type in assembly.GetTypes())
|
return builder.WithTools(
|
||||||
{
|
from t in toolAssembly.GetTypes()
|
||||||
if (type.GetCustomAttribute<McpToolTypeAttribute>() is null)
|
where t.GetCustomAttribute<McpServerToolTypeAttribute>() is not null
|
||||||
{
|
select t);
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var method in type.GetMethods(BindingFlags.Public | BindingFlags.Static))
|
|
||||||
{
|
|
||||||
if (method.GetCustomAttribute<McpToolAttribute>() is not null)
|
|
||||||
{
|
|
||||||
toolTypes.Add(type);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return toolTypes.Count > 0 ?
|
|
||||||
WithTools(builder, toolTypes) :
|
|
||||||
builder;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void PopulateFunctions(
|
private static IEnumerable<MethodInfo> GetToolMethods(
|
||||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type toolType,
|
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods)] Type toolType) =>
|
||||||
List<AIFunction> functions)
|
from method in toolType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static)
|
||||||
{
|
where method.GetCustomAttribute<McpServerToolAttribute>() is not null
|
||||||
foreach (var method in toolType.GetMethods(BindingFlags.Public | BindingFlags.Static))
|
select method;
|
||||||
{
|
|
||||||
if (method.GetCustomAttribute<McpToolAttribute>() is not { } attribute)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
functions.Add(AIFunctionFactory.Create(method, target: null, new()
|
|
||||||
{
|
|
||||||
Name = attribute.Name ?? method.Name,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using ModelContextProtocol.Protocol.Types;
|
|
||||||
using ModelContextProtocol.Server;
|
using ModelContextProtocol.Server;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using ModelContextProtocol.Utils;
|
using ModelContextProtocol.Utils;
|
||||||
|
|
@ -7,30 +6,66 @@ using ModelContextProtocol.Utils;
|
||||||
namespace ModelContextProtocol.Configuration;
|
namespace ModelContextProtocol.Configuration;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Configures the McpServerOptions using provided server handlers.
|
/// Configures the McpServerOptions using addition services from DI.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="serverHandlers">The server handlers configuration options.</param>
|
/// <param name="serverHandlers">The server handlers configuration options.</param>
|
||||||
internal sealed class McpServerOptionsSetup(IOptions<McpServerHandlers> serverHandlers) : IConfigureOptions<McpServerOptions>
|
/// <param name="serverTools">Tools individually registered.</param>
|
||||||
|
internal sealed class McpServerOptionsSetup(
|
||||||
|
IOptions<McpServerHandlers> serverHandlers,
|
||||||
|
IEnumerable<McpServerTool> serverTools) : IConfigureOptions<McpServerOptions>
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Configures the given McpServerOptions instance by setting server information
|
/// Configures the given McpServerOptions instance by setting server information
|
||||||
/// and applying custom server handlers.
|
/// and applying custom server handlers and tools.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="options">The options instance to be configured.</param>
|
/// <param name="options">The options instance to be configured.</param>
|
||||||
public void Configure(McpServerOptions options)
|
public void Configure(McpServerOptions options)
|
||||||
{
|
{
|
||||||
Throw.IfNull(options);
|
Throw.IfNull(options);
|
||||||
|
|
||||||
var assemblyName = Assembly.GetEntryAssembly()?.GetName();
|
// Configure the option's server information based on the current process,
|
||||||
|
// if it otherwise lacks server information.
|
||||||
// Set server information based on the entry assembly
|
var assemblyName = (Assembly.GetEntryAssembly() ?? Assembly.GetCallingAssembly()).GetName();
|
||||||
options.ServerInfo = new Implementation
|
if (options.ServerInfo is not { } serverInfo ||
|
||||||
|
serverInfo.Name is null ||
|
||||||
|
serverInfo.Version is null)
|
||||||
{
|
{
|
||||||
Name = assemblyName?.Name ?? "McpServer",
|
options.ServerInfo = options.ServerInfo is null ?
|
||||||
Version = assemblyName?.Version?.ToString() ?? "1.0.0",
|
new()
|
||||||
};
|
{
|
||||||
|
Name = assemblyName.Name ?? "McpServer",
|
||||||
|
Version = assemblyName.Version?.ToString() ?? "1.0.0",
|
||||||
|
} :
|
||||||
|
options.ServerInfo with
|
||||||
|
{
|
||||||
|
Name = options.ServerInfo.Name ?? assemblyName.Name ?? "McpServer",
|
||||||
|
Version = options.ServerInfo.Version ?? assemblyName.Version?.ToString() ?? "1.0.0",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Apply custom server handlers
|
// Collect all of the provided tools into a tools collection. If the options already has
|
||||||
|
// a collection, add to it, otherwise create a new one. We want to maintain the identity
|
||||||
|
// of an existing collection in case someone has provided their own derived type, wants
|
||||||
|
// change notifications, etc.
|
||||||
|
McpServerToolCollection toolsCollection = options.Capabilities?.Tools?.ToolCollection ?? [];
|
||||||
|
foreach (var tool in serverTools)
|
||||||
|
{
|
||||||
|
toolsCollection.TryAdd(tool);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!toolsCollection.IsEmpty)
|
||||||
|
{
|
||||||
|
options.Capabilities = options.Capabilities is null ?
|
||||||
|
new() { Tools = new() { ToolCollection = toolsCollection } } :
|
||||||
|
options.Capabilities with
|
||||||
|
{
|
||||||
|
Tools = options.Capabilities.Tools is null ?
|
||||||
|
new() { ToolCollection = toolsCollection } :
|
||||||
|
options.Capabilities.Tools with { ToolCollection = toolsCollection },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply custom server handlers.
|
||||||
serverHandlers.Value.OverwriteWithSetHandlers(options);
|
serverHandlers.Value.OverwriteWithSetHandlers(options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ public sealed class StdioServerTransport : TransportBase, IServerTransport
|
||||||
private readonly TextReader _stdInReader;
|
private readonly TextReader _stdInReader;
|
||||||
private readonly Stream _stdOutStream;
|
private readonly Stream _stdOutStream;
|
||||||
|
|
||||||
|
private SemaphoreSlim _sendLock = new(1, 1);
|
||||||
private Task? _readTask;
|
private Task? _readTask;
|
||||||
private CancellationTokenSource? _shutdownCts;
|
private CancellationTokenSource? _shutdownCts;
|
||||||
|
|
||||||
|
|
@ -137,6 +138,8 @@ public sealed class StdioServerTransport : TransportBase, IServerTransport
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public override async Task SendMessageAsync(IJsonRpcMessage message, CancellationToken cancellationToken = default)
|
public override async Task SendMessageAsync(IJsonRpcMessage message, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
using var _ = await _sendLock.LockAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
if (!IsConnected)
|
if (!IsConnected)
|
||||||
{
|
{
|
||||||
_logger.TransportNotConnected(EndpointName);
|
_logger.TransportNotConnected(EndpointName);
|
||||||
|
|
|
||||||
|
|
@ -155,7 +155,7 @@ public record ResourcesCapability
|
||||||
public record ToolsCapability
|
public record ToolsCapability
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether this server supports notifications for changes to the tool list.
|
/// Gets or sets whether this server supports notifications for changes to the tool list.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonPropertyName("listChanged")]
|
[JsonPropertyName("listChanged")]
|
||||||
public bool? ListChanged { get; init; }
|
public bool? ListChanged { get; init; }
|
||||||
|
|
@ -171,4 +171,16 @@ public record ToolsCapability
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public Func<RequestContext<CallToolRequestParams>, CancellationToken, Task<CallToolResponse>>? CallToolHandler { get; init; }
|
public Func<RequestContext<CallToolRequestParams>, CancellationToken, Task<CallToolResponse>>? CallToolHandler { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Gets or sets a collection of tools served by the server.</summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Tools will specified via <see cref="ToolCollection"/> augment the <see cref="ListToolsHandler"/> and
|
||||||
|
/// <see cref="CallToolHandler"/>, if provided. ListTools requests will output information about every tool
|
||||||
|
/// in <see cref="ToolCollection"/> and then also any tools output by <see cref="ListToolsHandler"/>, if it's
|
||||||
|
/// non-<see langword="null"/>. CallTool requests will first check <see cref="ToolCollection"/> for the tool
|
||||||
|
/// being requested, and if the tool is not found in the <see cref="ToolCollection"/>, any specified <see cref="CallToolHandler"/>
|
||||||
|
/// will be invoked as a fallback.
|
||||||
|
/// </remarks>
|
||||||
|
[JsonIgnore]
|
||||||
|
public McpServerToolCollection? ToolCollection { get; init; }
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
using ModelContextProtocol.Protocol.Types;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
|
|
||||||
namespace ModelContextProtocol.Protocol.Types;
|
namespace ModelContextProtocol.Protocol.Types;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@ namespace ModelContextProtocol.Protocol.Types;
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class Tool
|
public class Tool
|
||||||
{
|
{
|
||||||
|
private JsonElement _inputSchema = McpJsonUtilities.DefaultMcpToolSchema;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The name of the tool.
|
/// The name of the tool.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -42,6 +44,4 @@ public class Tool
|
||||||
_inputSchema = value;
|
_inputSchema = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private JsonElement _inputSchema = McpJsonUtilities.DefaultMcpToolSchema;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue