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).
|
||||
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"]
|
||||
}
|
||||
},
|
||||
"""),
|
||||
}
|
||||
]
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ using ModelContextProtocol;
|
|||
using AspNetCoreSseServer;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
builder.Services.AddMcpServer().WithTools();
|
||||
builder.Services.AddMcpServer().WithToolsFromAssembly();
|
||||
var app = builder.Build();
|
||||
|
||||
app.MapGet("/", () => "Hello World!");
|
||||
|
|
|
|||
|
|
@ -3,10 +3,10 @@ using System.ComponentModel;
|
|||
|
||||
namespace TestServerWithHosting.Tools;
|
||||
|
||||
[McpToolType]
|
||||
[McpServerToolType]
|
||||
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)
|
||||
{
|
||||
return "hello " + message;
|
||||
|
|
|
|||
|
|
@ -1,51 +1,36 @@
|
|||
using ModelContextProtocol.Protocol.Types;
|
||||
using Microsoft.Extensions.AI;
|
||||
using ModelContextProtocol.Server;
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace TestServerWithHosting.Tools;
|
||||
|
||||
/// <summary>
|
||||
/// This tool uses depenency injection and async method
|
||||
/// This tool uses dependency injection and async method
|
||||
/// </summary>
|
||||
[McpToolType]
|
||||
public class SampleLlmTool
|
||||
[McpServerToolType]
|
||||
public static class SampleLlmTool
|
||||
{
|
||||
private readonly IMcpServer _server;
|
||||
|
||||
public SampleLlmTool(IMcpServer server)
|
||||
{
|
||||
_server = server ?? throw new ArgumentNullException(nameof(server));
|
||||
}
|
||||
|
||||
[McpTool("sampleLLM"), Description("Samples from an LLM using MCP's sampling feature")]
|
||||
public async Task<string> SampleLLM(
|
||||
[McpServerTool("sampleLLM"), Description("Samples from an LLM using MCP's sampling feature")]
|
||||
public static async Task<string> SampleLLM(
|
||||
IMcpServer thisServer,
|
||||
[Description("The prompt to send to the LLM")] string prompt,
|
||||
[Description("Maximum number of tokens to generate")] int maxTokens,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var samplingParams = CreateRequestSamplingParams(prompt ?? string.Empty, "sampleLLM", maxTokens);
|
||||
var sampleResult = await _server.RequestSamplingAsync(samplingParams, cancellationToken);
|
||||
ChatMessage[] messages =
|
||||
[
|
||||
new(ChatRole.System, "You are a helpful test server."),
|
||||
new(ChatRole.User, prompt),
|
||||
];
|
||||
|
||||
return $"LLM sampling result: {sampleResult.Content.Text}";
|
||||
}
|
||||
|
||||
private static CreateMessageRequestParams CreateRequestSamplingParams(string context, string uri, int maxTokens = 100)
|
||||
{
|
||||
return new CreateMessageRequestParams()
|
||||
ChatOptions options = new()
|
||||
{
|
||||
Messages = [new SamplingMessage()
|
||||
{
|
||||
Role = Role.User,
|
||||
Content = new Content()
|
||||
{
|
||||
Type = "text",
|
||||
Text = $"Resource {uri} context: {context}"
|
||||
}
|
||||
}],
|
||||
SystemPrompt = "You are a helpful test server.",
|
||||
MaxTokens = maxTokens,
|
||||
MaxOutputTokens = maxTokens,
|
||||
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.OpenAI" />
|
||||
<PackageReference Include="Anthropic.SDK" />
|
||||
<PackageReference Include="System.Linq.AsyncEnumerable" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ var mcpClient = await McpClientFactory.CreateAsync(
|
|||
|
||||
// Get all available tools
|
||||
Console.WriteLine("Tools available:");
|
||||
var tools = await mcpClient.GetAIFunctionsAsync();
|
||||
var tools = await mcpClient.ListToolsAsync();
|
||||
foreach (var tool in tools)
|
||||
{
|
||||
Console.WriteLine($" {tool}");
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ try
|
|||
builder.Services.AddSerilog();
|
||||
builder.Services.AddMcpServer()
|
||||
.WithStdioServerTransport()
|
||||
.WithTools();
|
||||
.WithToolsFromAssembly();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
|
|
|
|||
|
|
@ -3,10 +3,10 @@ using System.ComponentModel;
|
|||
|
||||
namespace TestServerWithHosting.Tools;
|
||||
|
||||
[McpToolType]
|
||||
[McpServerToolType]
|
||||
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)
|
||||
{
|
||||
return "hello " + message;
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ namespace TestServerWithHosting.Tools;
|
|||
/// <summary>
|
||||
/// This tool uses depenency injection and async method
|
||||
/// </summary>
|
||||
[McpToolType]
|
||||
[McpServerToolType]
|
||||
public class SampleLlmTool
|
||||
{
|
||||
private readonly IMcpServer _server;
|
||||
|
|
@ -17,7 +17,7 @@ public class SampleLlmTool
|
|||
_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(
|
||||
[Description("The prompt to send to the LLM")] string prompt,
|
||||
[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;
|
||||
}
|
||||
|
||||
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.Json;
|
||||
using Microsoft.Extensions.AI;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.Json;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
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)
|
||||
{
|
||||
Throw.IfNull(client);
|
||||
Throw.IfNullOrWhiteSpace(method);
|
||||
|
||||
return client.SendMessageAsync(
|
||||
new JsonRpcNotification { Method = method, Params = parameters },
|
||||
|
|
@ -45,42 +46,67 @@ public static class McpClientExtensions
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a sequence of available tools from the server.
|
||||
/// Retrieves a list of 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 tool information.</returns>
|
||||
public static async IAsyncEnumerable<Tool> ListToolsAsync(
|
||||
this IMcpClient client, [EnumeratorCancellation] 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)
|
||||
/// <returns>A list of all available tools.</returns>
|
||||
public static async Task<IList<McpClientTool>> ListToolsAsync(
|
||||
this IMcpClient client, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(client);
|
||||
|
||||
return client.SendRequestAsync<ListToolsResult>(
|
||||
CreateRequest("tools/list", CreateCursorDictionary(cursor)),
|
||||
cancellationToken);
|
||||
List<McpClientTool>? tools = null;
|
||||
string? cursor = null;
|
||||
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>
|
||||
|
|
@ -88,40 +114,69 @@ public static class McpClientExtensions
|
|||
/// </summary>
|
||||
/// <param name="client">The client.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the operation.</param>
|
||||
/// <returns>An asynchronous sequence of prompt information.</returns>
|
||||
public static async IAsyncEnumerable<Prompt> ListPromptsAsync(
|
||||
this IMcpClient client, [EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
/// <returns>A list of all available prompts.</returns>
|
||||
public static async Task<IList<Prompt>> ListPromptsAsync(
|
||||
this IMcpClient client, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(client);
|
||||
|
||||
List<Prompt>? prompts = null;
|
||||
|
||||
string? cursor = null;
|
||||
do
|
||||
{
|
||||
var prompts = await ListPromptsAsync(client, cursor, cancellationToken).ConfigureAwait(false);
|
||||
foreach (var prompt in prompts.Prompts)
|
||||
var promptResults = await client.SendRequestAsync<ListPromptsResult>(
|
||||
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;
|
||||
}
|
||||
|
||||
cursor = prompts.NextCursor;
|
||||
cursor = promptResults.NextCursor;
|
||||
}
|
||||
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>
|
||||
/// Retrieves a specific prompt with optional arguments.
|
||||
/// </summary>
|
||||
|
|
@ -130,9 +185,11 @@ public static class McpClientExtensions
|
|||
/// <param name="arguments">Optional arguments for the prompt</param>
|
||||
/// <param name="cancellationToken">A token to cancel the operation.</param>
|
||||
/// <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.IfNullOrWhiteSpace(name);
|
||||
|
||||
return client.SendRequestAsync<GetPromptResult>(
|
||||
CreateRequest("prompts/get", CreateParametersDictionary(name, arguments)),
|
||||
|
|
@ -140,24 +197,69 @@ public static class McpClientExtensions
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a sequence of available resource templates from the server.
|
||||
/// Retrieves a list of 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 resource template information.</returns>
|
||||
public static async IAsyncEnumerable<ResourceTemplate> ListResourceTemplatesAsync(
|
||||
this IMcpClient client, [EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
/// <returns>A list of all available resource templates.</returns>
|
||||
public static async Task<IList<ResourceTemplate>> ListResourceTemplatesAsync(
|
||||
this IMcpClient client, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(client);
|
||||
|
||||
List<ResourceTemplate>? templates = null;
|
||||
|
||||
string? cursor = null;
|
||||
do
|
||||
{
|
||||
var resources = await ListResourceTemplatesAsync(client, cursor, cancellationToken).ConfigureAwait(false);
|
||||
foreach (var resource in resources.ResourceTemplates)
|
||||
var templateResults = await client.SendRequestAsync<ListResourceTemplatesResult>(
|
||||
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);
|
||||
}
|
||||
|
|
@ -166,64 +268,81 @@ public static class McpClientExtensions
|
|||
/// 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<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);
|
||||
|
||||
return client.SendRequestAsync<ListResourceTemplatesResult>(
|
||||
CreateRequest("resources/templates/list", CreateCursorDictionary(cursor)),
|
||||
cancellationToken);
|
||||
}
|
||||
List<Resource>? resources = null;
|
||||
|
||||
/// <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;
|
||||
do
|
||||
{
|
||||
var resources = await ListResourcesAsync(client, cursor, cancellationToken).ConfigureAwait(false);
|
||||
foreach (var resource in resources.Resources)
|
||||
var resourceResults = await client.SendRequestAsync<ListResourcesResult>(
|
||||
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;
|
||||
}
|
||||
|
||||
cursor = resources.NextCursor;
|
||||
cursor = resourceResults.NextCursor;
|
||||
}
|
||||
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>
|
||||
/// Reads a resource from the server.
|
||||
/// </summary>
|
||||
/// <param name="client">The client.</param>
|
||||
/// <param name="uri">The uri of the resource.</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.IfNullOrWhiteSpace(uri);
|
||||
|
||||
return client.SendRequestAsync<ReadResourceResult>(
|
||||
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)
|
||||
{
|
||||
Throw.IfNull(client);
|
||||
Throw.IfNullOrWhiteSpace(uri);
|
||||
|
||||
return client.SendRequestAsync<EmptyResult>(
|
||||
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)
|
||||
{
|
||||
Throw.IfNull(client);
|
||||
Throw.IfNullOrWhiteSpace(uri);
|
||||
|
||||
return client.SendRequestAsync<EmptyResult>(
|
||||
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="cancellationToken">A token to cancel the operation.</param>
|
||||
/// <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(toolName);
|
||||
|
||||
return client.SendRequestAsync<CallToolResponse>(
|
||||
CreateRequest("tools/call", CreateParametersDictionary(toolName, arguments)),
|
||||
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>
|
||||
/// Converts the contents of a <see cref="CreateMessageRequestParams"/> into a pair of
|
||||
/// <see cref="IEnumerable{ChatMessage}"/> and <see cref="ChatOptions"/> instances to use
|
||||
|
|
@ -428,12 +518,7 @@ public static class McpClientExtensions
|
|||
{
|
||||
Type = "image",
|
||||
MimeType = dc.MediaType,
|
||||
Data = Convert.ToBase64String(dc.Data
|
||||
#if NET
|
||||
.Span),
|
||||
#else
|
||||
.ToArray()),
|
||||
#endif
|
||||
Data = dc.GetBase64Data(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -499,7 +584,8 @@ public static class McpClientExtensions
|
|||
private static Dictionary<string, object?>? CreateCursorDictionary(string? cursor) =>
|
||||
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()
|
||||
{
|
||||
|
|
@ -526,20 +612,20 @@ public static class McpClientExtensions
|
|||
/// <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)
|
||||
{
|
||||
Throw.IfNull(arguments);
|
||||
|
||||
Dictionary<string, object> argDict = [];
|
||||
foreach (var arg in arguments)
|
||||
{
|
||||
if (arg.Value is not null)
|
||||
{
|
||||
argDict[arg.Key] = arg.Value;
|
||||
}
|
||||
}
|
||||
IReadOnlyDictionary<string, object?> argDict =
|
||||
arguments as IReadOnlyDictionary<string, object?> ??
|
||||
#if NET
|
||||
arguments.ToDictionary();
|
||||
#else
|
||||
arguments.ToDictionary(kv => kv.Key, kv => kv.Value);
|
||||
#endif
|
||||
|
||||
CallToolResponse result = await client.CallToolAsync(tool.Name, argDict, cancellationToken).ConfigureAwait(false);
|
||||
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.Protocol.Types;
|
||||
using ModelContextProtocol.Server;
|
||||
using ModelContextProtocol.Utils;
|
||||
using ModelContextProtocol.Utils.Json;
|
||||
using Microsoft.Extensions.AI;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace ModelContextProtocol;
|
||||
|
||||
|
|
@ -23,23 +20,17 @@ public static partial class McpServerBuilderExtensions
|
|||
/// <typeparam name="TTool">The tool type.</typeparam>
|
||||
/// <param name="builder">The builder instance.</param>
|
||||
/// <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);
|
||||
List<AIFunction> functions = [];
|
||||
|
||||
PopulateFunctions(typeof(TTool), functions);
|
||||
return WithTools(builder, functions);
|
||||
}
|
||||
/// <summary>
|
||||
/// Adds all tools marked with <see cref="McpToolTypeAttribute"/> from the current assembly to the server.
|
||||
/// </summary>
|
||||
/// <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());
|
||||
foreach (var toolMethod in GetToolMethods(typeof(TTool)))
|
||||
{
|
||||
builder.Services.AddSingleton(services => McpServerTool.Create(toolMethod, services: services));
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -55,147 +46,42 @@ public static partial class McpServerBuilderExtensions
|
|||
Throw.IfNull(builder);
|
||||
Throw.IfNull(toolTypes);
|
||||
|
||||
List<AIFunction> functions = [];
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// <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>
|
||||
[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())
|
||||
{
|
||||
if (type.GetCustomAttribute<McpToolTypeAttribute>() is null)
|
||||
{
|
||||
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;
|
||||
return builder.WithTools(
|
||||
from t in toolAssembly.GetTypes()
|
||||
where t.GetCustomAttribute<McpServerToolTypeAttribute>() is not null
|
||||
select t);
|
||||
}
|
||||
|
||||
private static void PopulateFunctions(
|
||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type toolType,
|
||||
List<AIFunction> functions)
|
||||
{
|
||||
foreach (var method in toolType.GetMethods(BindingFlags.Public | BindingFlags.Static))
|
||||
{
|
||||
if (method.GetCustomAttribute<McpToolAttribute>() is not { } attribute)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
functions.Add(AIFunctionFactory.Create(method, target: null, new()
|
||||
{
|
||||
Name = attribute.Name ?? method.Name,
|
||||
}));
|
||||
}
|
||||
}
|
||||
private static IEnumerable<MethodInfo> GetToolMethods(
|
||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods)] Type toolType) =>
|
||||
from method in toolType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static)
|
||||
where method.GetCustomAttribute<McpServerToolAttribute>() is not null
|
||||
select method;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
using System.Reflection;
|
||||
using ModelContextProtocol.Protocol.Types;
|
||||
using ModelContextProtocol.Server;
|
||||
using Microsoft.Extensions.Options;
|
||||
using ModelContextProtocol.Utils;
|
||||
|
|
@ -7,30 +6,66 @@ using ModelContextProtocol.Utils;
|
|||
namespace ModelContextProtocol.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Configures the McpServerOptions using provided server handlers.
|
||||
/// Configures the McpServerOptions using addition services from DI.
|
||||
/// </summary>
|
||||
/// <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>
|
||||
/// Configures the given McpServerOptions instance by setting server information
|
||||
/// and applying custom server handlers.
|
||||
/// and applying custom server handlers and tools.
|
||||
/// </summary>
|
||||
/// <param name="options">The options instance to be configured.</param>
|
||||
public void Configure(McpServerOptions options)
|
||||
{
|
||||
Throw.IfNull(options);
|
||||
|
||||
var assemblyName = Assembly.GetEntryAssembly()?.GetName();
|
||||
|
||||
// Set server information based on the entry assembly
|
||||
options.ServerInfo = new Implementation
|
||||
// Configure the option's server information based on the current process,
|
||||
// if it otherwise lacks server information.
|
||||
var assemblyName = (Assembly.GetEntryAssembly() ?? Assembly.GetCallingAssembly()).GetName();
|
||||
if (options.ServerInfo is not { } serverInfo ||
|
||||
serverInfo.Name is null ||
|
||||
serverInfo.Version is null)
|
||||
{
|
||||
Name = assemblyName?.Name ?? "McpServer",
|
||||
Version = assemblyName?.Version?.ToString() ?? "1.0.0",
|
||||
};
|
||||
options.ServerInfo = options.ServerInfo is null ?
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ public sealed class StdioServerTransport : TransportBase, IServerTransport
|
|||
private readonly TextReader _stdInReader;
|
||||
private readonly Stream _stdOutStream;
|
||||
|
||||
private SemaphoreSlim _sendLock = new(1, 1);
|
||||
private Task? _readTask;
|
||||
private CancellationTokenSource? _shutdownCts;
|
||||
|
||||
|
|
@ -137,6 +138,8 @@ public sealed class StdioServerTransport : TransportBase, IServerTransport
|
|||
/// <inheritdoc/>
|
||||
public override async Task SendMessageAsync(IJsonRpcMessage message, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var _ = await _sendLock.LockAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (!IsConnected)
|
||||
{
|
||||
_logger.TransportNotConnected(EndpointName);
|
||||
|
|
|
|||
|
|
@ -16,5 +16,5 @@ public class CallToolRequestParams
|
|||
/// Optional arguments to pass to the tool.
|
||||
/// </summary>
|
||||
[System.Text.Json.Serialization.JsonPropertyName("arguments")]
|
||||
public Dictionary<string,object>? Arguments { get; init; }
|
||||
public Dictionary<string,object>? Arguments { get; init; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -155,7 +155,7 @@ public record ResourcesCapability
|
|||
public record ToolsCapability
|
||||
{
|
||||
/// <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>
|
||||
[JsonPropertyName("listChanged")]
|
||||
public bool? ListChanged { get; init; }
|
||||
|
|
@ -171,4 +171,16 @@ public record ToolsCapability
|
|||
/// </summary>
|
||||
[JsonIgnore]
|
||||
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;
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ namespace ModelContextProtocol.Protocol.Types;
|
|||
/// </summary>
|
||||
public class Tool
|
||||
{
|
||||
private JsonElement _inputSchema = McpJsonUtilities.DefaultMcpToolSchema;
|
||||
|
||||
/// <summary>
|
||||
/// The name of the tool.
|
||||
/// </summary>
|
||||
|
|
@ -42,6 +44,4 @@ public class Tool
|
|||
_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