Support progress notifications with McpClientTool and McpClientExtensions.CallToolAsync (#312)
This commit is contained in:
parent
90748b1198
commit
227f440ab9
8 changed files with 174 additions and 47 deletions
|
|
@ -783,9 +783,14 @@ public static class McpClientExtensions
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="client">The client instance used to communicate with the MCP server.</param>
|
/// <param name="client">The client instance used to communicate with the MCP server.</param>
|
||||||
/// <param name="toolName">The name of the tool to call on the server..</param>
|
/// <param name="toolName">The name of the tool to call on the server..</param>
|
||||||
/// <param name="arguments">Optional dictionary of arguments to pass to the tool. Each key represents a parameter name,
|
/// <param name="arguments">An optional dictionary of arguments to pass to the tool. Each key represents a parameter name,
|
||||||
/// and its associated value represents the argument value.
|
/// and its associated value represents the argument value.
|
||||||
/// </param>
|
/// </param>
|
||||||
|
/// <param name="progress">
|
||||||
|
/// An optional <see cref="IProgress{T}"/> to have progress notifications reported to it. Setting this to a non-<see langword="null"/>
|
||||||
|
/// value will result in a progress token being included in the call, and any resulting progress notifications during the operation
|
||||||
|
/// routed to this instance.
|
||||||
|
/// </param>
|
||||||
/// <param name="serializerOptions">
|
/// <param name="serializerOptions">
|
||||||
/// The JSON serialization options governing argument serialization. If <see langword="null"/>, the default serialization options will be used.
|
/// The JSON serialization options governing argument serialization. If <see langword="null"/>, the default serialization options will be used.
|
||||||
/// </param>
|
/// </param>
|
||||||
|
|
@ -812,6 +817,7 @@ public static class McpClientExtensions
|
||||||
this IMcpClient client,
|
this IMcpClient client,
|
||||||
string toolName,
|
string toolName,
|
||||||
IReadOnlyDictionary<string, object?>? arguments = null,
|
IReadOnlyDictionary<string, object?>? arguments = null,
|
||||||
|
IProgress<ProgressNotificationValue>? progress = null,
|
||||||
JsonSerializerOptions? serializerOptions = null,
|
JsonSerializerOptions? serializerOptions = null,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
|
@ -820,12 +826,56 @@ public static class McpClientExtensions
|
||||||
serializerOptions ??= McpJsonUtilities.DefaultOptions;
|
serializerOptions ??= McpJsonUtilities.DefaultOptions;
|
||||||
serializerOptions.MakeReadOnly();
|
serializerOptions.MakeReadOnly();
|
||||||
|
|
||||||
|
if (progress is not null)
|
||||||
|
{
|
||||||
|
return SendRequestWithProgressAsync(client, toolName, arguments, progress, serializerOptions, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
return client.SendRequestAsync(
|
return client.SendRequestAsync(
|
||||||
RequestMethods.ToolsCall,
|
RequestMethods.ToolsCall,
|
||||||
new() { Name = toolName, Arguments = ToArgumentsDictionary(arguments, serializerOptions) },
|
new()
|
||||||
|
{
|
||||||
|
Name = toolName,
|
||||||
|
Arguments = ToArgumentsDictionary(arguments, serializerOptions),
|
||||||
|
},
|
||||||
McpJsonUtilities.JsonContext.Default.CallToolRequestParams,
|
McpJsonUtilities.JsonContext.Default.CallToolRequestParams,
|
||||||
McpJsonUtilities.JsonContext.Default.CallToolResponse,
|
McpJsonUtilities.JsonContext.Default.CallToolResponse,
|
||||||
cancellationToken: cancellationToken);
|
cancellationToken: cancellationToken);
|
||||||
|
|
||||||
|
static async Task<CallToolResponse> SendRequestWithProgressAsync(
|
||||||
|
IMcpClient client,
|
||||||
|
string toolName,
|
||||||
|
IReadOnlyDictionary<string, object?>? arguments,
|
||||||
|
IProgress<ProgressNotificationValue> progress,
|
||||||
|
JsonSerializerOptions serializerOptions,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
ProgressToken progressToken = new(Guid.NewGuid().ToString("N"));
|
||||||
|
|
||||||
|
await using var _ = client.RegisterNotificationHandler(NotificationMethods.ProgressNotification,
|
||||||
|
(notification, cancellationToken) =>
|
||||||
|
{
|
||||||
|
if (JsonSerializer.Deserialize(notification.Params, McpJsonUtilities.JsonContext.Default.ProgressNotification) is { } pn &&
|
||||||
|
pn.ProgressToken == progressToken)
|
||||||
|
{
|
||||||
|
progress.Report(pn.Progress);
|
||||||
|
}
|
||||||
|
|
||||||
|
return default;
|
||||||
|
}).ConfigureAwait(false);
|
||||||
|
|
||||||
|
return await client.SendRequestAsync(
|
||||||
|
RequestMethods.ToolsCall,
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Name = toolName,
|
||||||
|
Arguments = ToArgumentsDictionary(arguments, serializerOptions),
|
||||||
|
Meta = new() { ProgressToken = progressToken },
|
||||||
|
},
|
||||||
|
McpJsonUtilities.JsonContext.Default.CallToolRequestParams,
|
||||||
|
McpJsonUtilities.JsonContext.Default.CallToolResponse,
|
||||||
|
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
using ModelContextProtocol.Protocol.Types;
|
|
||||||
using ModelContextProtocol.Utils.Json;
|
|
||||||
using Microsoft.Extensions.AI;
|
using Microsoft.Extensions.AI;
|
||||||
using System.Text.Json;
|
using ModelContextProtocol.Protocol.Types;
|
||||||
|
using ModelContextProtocol.Utils;
|
||||||
|
using ModelContextProtocol.Utils.Json;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace ModelContextProtocol.Client;
|
namespace ModelContextProtocol.Client;
|
||||||
|
|
||||||
|
|
@ -36,14 +37,22 @@ public sealed class McpClientTool : AIFunction
|
||||||
private readonly IMcpClient _client;
|
private readonly IMcpClient _client;
|
||||||
private readonly string _name;
|
private readonly string _name;
|
||||||
private readonly string _description;
|
private readonly string _description;
|
||||||
|
private readonly IProgress<ProgressNotificationValue>? _progress;
|
||||||
|
|
||||||
internal McpClientTool(IMcpClient client, Tool tool, JsonSerializerOptions serializerOptions, string? name = null, string? description = null)
|
internal McpClientTool(
|
||||||
|
IMcpClient client,
|
||||||
|
Tool tool,
|
||||||
|
JsonSerializerOptions serializerOptions,
|
||||||
|
string? name = null,
|
||||||
|
string? description = null,
|
||||||
|
IProgress<ProgressNotificationValue>? progress = null)
|
||||||
{
|
{
|
||||||
_client = client;
|
_client = client;
|
||||||
ProtocolTool = tool;
|
ProtocolTool = tool;
|
||||||
JsonSerializerOptions = serializerOptions;
|
JsonSerializerOptions = serializerOptions;
|
||||||
_name = name ?? tool.Name;
|
_name = name ?? tool.Name;
|
||||||
_description = description ?? tool.Description ?? string.Empty;
|
_description = description ?? tool.Description ?? string.Empty;
|
||||||
|
_progress = progress;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -77,7 +86,7 @@ public sealed class McpClientTool : AIFunction
|
||||||
protected async override ValueTask<object?> InvokeCoreAsync(
|
protected async override ValueTask<object?> InvokeCoreAsync(
|
||||||
AIFunctionArguments arguments, CancellationToken cancellationToken)
|
AIFunctionArguments arguments, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
CallToolResponse result = await _client.CallToolAsync(ProtocolTool.Name, arguments, JsonSerializerOptions, cancellationToken: cancellationToken).ConfigureAwait(false);
|
CallToolResponse result = await _client.CallToolAsync(ProtocolTool.Name, arguments, _progress, JsonSerializerOptions, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||||
return JsonSerializer.SerializeToElement(result, McpJsonUtilities.JsonContext.Default.CallToolResponse);
|
return JsonSerializer.SerializeToElement(result, McpJsonUtilities.JsonContext.Default.CallToolResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -107,7 +116,7 @@ public sealed class McpClientTool : AIFunction
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public McpClientTool WithName(string name)
|
public McpClientTool WithName(string name)
|
||||||
{
|
{
|
||||||
return new McpClientTool(_client, ProtocolTool, JsonSerializerOptions, name, _description);
|
return new McpClientTool(_client, ProtocolTool, JsonSerializerOptions, name, _description, _progress);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -133,6 +142,30 @@ public sealed class McpClientTool : AIFunction
|
||||||
/// <returns>A new instance of <see cref="McpClientTool"/> with the provided description.</returns>
|
/// <returns>A new instance of <see cref="McpClientTool"/> with the provided description.</returns>
|
||||||
public McpClientTool WithDescription(string description)
|
public McpClientTool WithDescription(string description)
|
||||||
{
|
{
|
||||||
return new McpClientTool(_client, ProtocolTool, JsonSerializerOptions, _name, description);
|
return new McpClientTool(_client, ProtocolTool, JsonSerializerOptions, _name, description, _progress);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new instance of the tool but modified to report progress via the specified <see cref="IProgress{T}"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="progress">The <see cref="IProgress{T}"/> to which progress notifications should be reported.</param>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// Adding an <see cref="IProgress{T}"/> to the tool does not impact how it is reported to any AI model.
|
||||||
|
/// Rather, when the tool is invoked, the request to the MCP server will include a unique progress token,
|
||||||
|
/// and any progress notifications issued by the server with that progress token while the operation is in
|
||||||
|
/// flight will be reported to the <paramref name="progress"/> instance.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// Only one <see cref="IProgress{T}"/> can be specified at a time. Calling <see cref="WithProgress"/> again
|
||||||
|
/// will overwrite any previously specified progress instance.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
/// <returns>A new instance of <see cref="McpClientTool"/>, configured with the provided progress instance.</returns>
|
||||||
|
public McpClientTool WithProgress(IProgress<ProgressNotificationValue> progress)
|
||||||
|
{
|
||||||
|
Throw.IfNull(progress);
|
||||||
|
|
||||||
|
return new McpClientTool(_client, ProtocolTool, JsonSerializerOptions, _name, _description, progress);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -29,7 +29,6 @@ public class McpClientExtensionsTests : ClientServerTestBase
|
||||||
}
|
}
|
||||||
services.AddSingleton(McpServerTool.Create([McpServerTool(Destructive = false, OpenWorld = true)] (string i) => $"{i} Result", new() { Name = "ValuesSetViaAttr" }));
|
services.AddSingleton(McpServerTool.Create([McpServerTool(Destructive = false, OpenWorld = true)] (string i) => $"{i} Result", new() { Name = "ValuesSetViaAttr" }));
|
||||||
services.AddSingleton(McpServerTool.Create([McpServerTool(Destructive = false, OpenWorld = true)] (string i) => $"{i} Result", new() { Name = "ValuesSetViaOptions", Destructive = true, OpenWorld = false, ReadOnly = true }));
|
services.AddSingleton(McpServerTool.Create([McpServerTool(Destructive = false, OpenWorld = true)] (string i) => $"{i} Result", new() { Name = "ValuesSetViaOptions", Destructive = true, OpenWorld = false, ReadOnly = true }));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
|
|
@ -209,7 +208,7 @@ public class McpClientExtensionsTests : ClientServerTestBase
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ListToolsAsync_AllToolsReturned()
|
public async Task ListToolsAsync_AllToolsReturned()
|
||||||
{
|
{
|
||||||
IMcpClient client = await CreateMcpClientForServer();
|
await using IMcpClient client = await CreateMcpClientForServer();
|
||||||
|
|
||||||
var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
|
var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
|
||||||
Assert.Equal(12, tools.Count);
|
Assert.Equal(12, tools.Count);
|
||||||
|
|
@ -235,7 +234,7 @@ public class McpClientExtensionsTests : ClientServerTestBase
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task EnumerateToolsAsync_AllToolsReturned()
|
public async Task EnumerateToolsAsync_AllToolsReturned()
|
||||||
{
|
{
|
||||||
IMcpClient client = await CreateMcpClientForServer();
|
await using IMcpClient client = await CreateMcpClientForServer();
|
||||||
|
|
||||||
await foreach (var tool in client.EnumerateToolsAsync(cancellationToken: TestContext.Current.CancellationToken))
|
await foreach (var tool in client.EnumerateToolsAsync(cancellationToken: TestContext.Current.CancellationToken))
|
||||||
{
|
{
|
||||||
|
|
@ -254,7 +253,7 @@ public class McpClientExtensionsTests : ClientServerTestBase
|
||||||
public async Task EnumerateToolsAsync_FlowsJsonSerializerOptions()
|
public async Task EnumerateToolsAsync_FlowsJsonSerializerOptions()
|
||||||
{
|
{
|
||||||
JsonSerializerOptions options = new(JsonSerializerOptions.Default);
|
JsonSerializerOptions options = new(JsonSerializerOptions.Default);
|
||||||
IMcpClient client = await CreateMcpClientForServer();
|
await using IMcpClient client = await CreateMcpClientForServer();
|
||||||
bool hasTools = false;
|
bool hasTools = false;
|
||||||
|
|
||||||
await foreach (var tool in client.EnumerateToolsAsync(options, TestContext.Current.CancellationToken))
|
await foreach (var tool in client.EnumerateToolsAsync(options, TestContext.Current.CancellationToken))
|
||||||
|
|
@ -275,7 +274,7 @@ public class McpClientExtensionsTests : ClientServerTestBase
|
||||||
public async Task EnumerateToolsAsync_HonorsJsonSerializerOptions()
|
public async Task EnumerateToolsAsync_HonorsJsonSerializerOptions()
|
||||||
{
|
{
|
||||||
JsonSerializerOptions emptyOptions = new() { TypeInfoResolver = JsonTypeInfoResolver.Combine() };
|
JsonSerializerOptions emptyOptions = new() { TypeInfoResolver = JsonTypeInfoResolver.Combine() };
|
||||||
IMcpClient client = await CreateMcpClientForServer();
|
await using IMcpClient client = await CreateMcpClientForServer();
|
||||||
|
|
||||||
var tool = (await client.ListToolsAsync(emptyOptions, TestContext.Current.CancellationToken)).First();
|
var tool = (await client.ListToolsAsync(emptyOptions, TestContext.Current.CancellationToken)).First();
|
||||||
await Assert.ThrowsAsync<NotSupportedException>(async () => await tool.InvokeAsync(new() { ["i"] = 42 }, TestContext.Current.CancellationToken));
|
await Assert.ThrowsAsync<NotSupportedException>(async () => await tool.InvokeAsync(new() { ["i"] = 42 }, TestContext.Current.CancellationToken));
|
||||||
|
|
@ -285,7 +284,7 @@ public class McpClientExtensionsTests : ClientServerTestBase
|
||||||
public async Task SendRequestAsync_HonorsJsonSerializerOptions()
|
public async Task SendRequestAsync_HonorsJsonSerializerOptions()
|
||||||
{
|
{
|
||||||
JsonSerializerOptions emptyOptions = new() { TypeInfoResolver = JsonTypeInfoResolver.Combine() };
|
JsonSerializerOptions emptyOptions = new() { TypeInfoResolver = JsonTypeInfoResolver.Combine() };
|
||||||
IMcpClient client = await CreateMcpClientForServer();
|
await using IMcpClient client = await CreateMcpClientForServer();
|
||||||
|
|
||||||
await Assert.ThrowsAsync<NotSupportedException>(() => client.SendRequestAsync<CallToolRequestParams, CallToolResponse>("Method4", new() { Name = "tool" }, emptyOptions, cancellationToken: TestContext.Current.CancellationToken));
|
await Assert.ThrowsAsync<NotSupportedException>(() => client.SendRequestAsync<CallToolRequestParams, CallToolResponse>("Method4", new() { Name = "tool" }, emptyOptions, cancellationToken: TestContext.Current.CancellationToken));
|
||||||
}
|
}
|
||||||
|
|
@ -294,7 +293,7 @@ public class McpClientExtensionsTests : ClientServerTestBase
|
||||||
public async Task SendNotificationAsync_HonorsJsonSerializerOptions()
|
public async Task SendNotificationAsync_HonorsJsonSerializerOptions()
|
||||||
{
|
{
|
||||||
JsonSerializerOptions emptyOptions = new() { TypeInfoResolver = JsonTypeInfoResolver.Combine() };
|
JsonSerializerOptions emptyOptions = new() { TypeInfoResolver = JsonTypeInfoResolver.Combine() };
|
||||||
IMcpClient client = await CreateMcpClientForServer();
|
await using IMcpClient client = await CreateMcpClientForServer();
|
||||||
|
|
||||||
await Assert.ThrowsAsync<NotSupportedException>(() => client.SendNotificationAsync("Method4", new { Value = 42 }, emptyOptions, cancellationToken: TestContext.Current.CancellationToken));
|
await Assert.ThrowsAsync<NotSupportedException>(() => client.SendNotificationAsync("Method4", new { Value = 42 }, emptyOptions, cancellationToken: TestContext.Current.CancellationToken));
|
||||||
}
|
}
|
||||||
|
|
@ -303,7 +302,7 @@ public class McpClientExtensionsTests : ClientServerTestBase
|
||||||
public async Task GetPromptsAsync_HonorsJsonSerializerOptions()
|
public async Task GetPromptsAsync_HonorsJsonSerializerOptions()
|
||||||
{
|
{
|
||||||
JsonSerializerOptions emptyOptions = new() { TypeInfoResolver = JsonTypeInfoResolver.Combine() };
|
JsonSerializerOptions emptyOptions = new() { TypeInfoResolver = JsonTypeInfoResolver.Combine() };
|
||||||
IMcpClient client = await CreateMcpClientForServer();
|
await using IMcpClient client = await CreateMcpClientForServer();
|
||||||
|
|
||||||
await Assert.ThrowsAsync<NotSupportedException>(() => client.GetPromptAsync("Prompt", new Dictionary<string, object?> { ["i"] = 42 }, emptyOptions, cancellationToken: TestContext.Current.CancellationToken));
|
await Assert.ThrowsAsync<NotSupportedException>(() => client.GetPromptAsync("Prompt", new Dictionary<string, object?> { ["i"] = 42 }, emptyOptions, cancellationToken: TestContext.Current.CancellationToken));
|
||||||
}
|
}
|
||||||
|
|
@ -312,7 +311,7 @@ public class McpClientExtensionsTests : ClientServerTestBase
|
||||||
public async Task WithName_ChangesToolName()
|
public async Task WithName_ChangesToolName()
|
||||||
{
|
{
|
||||||
JsonSerializerOptions options = new(JsonSerializerOptions.Default);
|
JsonSerializerOptions options = new(JsonSerializerOptions.Default);
|
||||||
IMcpClient client = await CreateMcpClientForServer();
|
await using IMcpClient client = await CreateMcpClientForServer();
|
||||||
|
|
||||||
var tool = (await client.ListToolsAsync(options, TestContext.Current.CancellationToken)).First();
|
var tool = (await client.ListToolsAsync(options, TestContext.Current.CancellationToken)).First();
|
||||||
var originalName = tool.Name;
|
var originalName = tool.Name;
|
||||||
|
|
@ -327,7 +326,7 @@ public class McpClientExtensionsTests : ClientServerTestBase
|
||||||
public async Task WithDescription_ChangesToolDescription()
|
public async Task WithDescription_ChangesToolDescription()
|
||||||
{
|
{
|
||||||
JsonSerializerOptions options = new(JsonSerializerOptions.Default);
|
JsonSerializerOptions options = new(JsonSerializerOptions.Default);
|
||||||
IMcpClient client = await CreateMcpClientForServer();
|
await using IMcpClient client = await CreateMcpClientForServer();
|
||||||
var tool = (await client.ListToolsAsync(options, TestContext.Current.CancellationToken)).FirstOrDefault();
|
var tool = (await client.ListToolsAsync(options, TestContext.Current.CancellationToken)).FirstOrDefault();
|
||||||
var originalDescription = tool?.Description;
|
var originalDescription = tool?.Description;
|
||||||
var redescribedTool = tool?.WithDescription("ToolWithNewDescription");
|
var redescribedTool = tool?.WithDescription("ToolWithNewDescription");
|
||||||
|
|
@ -336,10 +335,55 @@ public class McpClientExtensionsTests : ClientServerTestBase
|
||||||
Assert.Equal(originalDescription, tool?.Description);
|
Assert.Equal(originalDescription, tool?.Description);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task WithProgress_ProgressReported()
|
||||||
|
{
|
||||||
|
const int TotalNotifications = 3;
|
||||||
|
int remainingProgress = TotalNotifications;
|
||||||
|
TaskCompletionSource<bool> allProgressReceived = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
|
||||||
|
Server.ServerOptions.Capabilities?.Tools?.ToolCollection?.Add(McpServerTool.Create(async (IProgress<ProgressNotificationValue> progress) =>
|
||||||
|
{
|
||||||
|
for (int i = 0; i < TotalNotifications; i++)
|
||||||
|
{
|
||||||
|
progress.Report(new ProgressNotificationValue { Progress = i * 10, Message = "making progress" });
|
||||||
|
await Task.Delay(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
await allProgressReceived.Task;
|
||||||
|
|
||||||
|
return 42;
|
||||||
|
}, new() { Name = "ProgressReporter" }));
|
||||||
|
|
||||||
|
await using IMcpClient client = await CreateMcpClientForServer();
|
||||||
|
|
||||||
|
var tool = (await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken)).First(t => t.Name == "ProgressReporter");
|
||||||
|
|
||||||
|
IProgress<ProgressNotificationValue> progress = new SynchronousProgress(value =>
|
||||||
|
{
|
||||||
|
Assert.True(value.Progress >= 0 && value.Progress <= 100);
|
||||||
|
Assert.Equal("making progress", value.Message);
|
||||||
|
if (Interlocked.Decrement(ref remainingProgress) == 0)
|
||||||
|
{
|
||||||
|
allProgressReceived.SetResult(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.Throws<ArgumentNullException>("progress", () => tool.WithProgress(null!));
|
||||||
|
|
||||||
|
var result = await tool.WithProgress(progress).InvokeAsync(cancellationToken: TestContext.Current.CancellationToken);
|
||||||
|
Assert.Contains("42", result?.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class SynchronousProgress(Action<ProgressNotificationValue> callback) : IProgress<ProgressNotificationValue>
|
||||||
|
{
|
||||||
|
public void Report(ProgressNotificationValue value) => callback(value);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task AsClientLoggerProvider_MessagesSentToClient()
|
public async Task AsClientLoggerProvider_MessagesSentToClient()
|
||||||
{
|
{
|
||||||
IMcpClient client = await CreateMcpClientForServer();
|
await using IMcpClient client = await CreateMcpClientForServer();
|
||||||
|
|
||||||
ILoggerProvider loggerProvider = Server.AsClientLoggerProvider();
|
ILoggerProvider loggerProvider = Server.AsClientLoggerProvider();
|
||||||
Assert.Throws<ArgumentNullException>("categoryName", () => loggerProvider.CreateLogger(null!));
|
Assert.Throws<ArgumentNullException>("categoryName", () => loggerProvider.CreateLogger(null!));
|
||||||
|
|
|
||||||
|
|
@ -96,7 +96,7 @@ public partial class McpServerBuilderExtensionsPromptsTests : ClientServerTestBa
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Can_List_And_Call_Registered_Prompts()
|
public async Task Can_List_And_Call_Registered_Prompts()
|
||||||
{
|
{
|
||||||
IMcpClient client = await CreateMcpClientForServer();
|
await using IMcpClient client = await CreateMcpClientForServer();
|
||||||
|
|
||||||
var prompts = await client.ListPromptsAsync(TestContext.Current.CancellationToken);
|
var prompts = await client.ListPromptsAsync(TestContext.Current.CancellationToken);
|
||||||
Assert.Equal(6, prompts.Count);
|
Assert.Equal(6, prompts.Count);
|
||||||
|
|
@ -125,7 +125,7 @@ public partial class McpServerBuilderExtensionsPromptsTests : ClientServerTestBa
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Can_Be_Notified_Of_Prompt_Changes()
|
public async Task Can_Be_Notified_Of_Prompt_Changes()
|
||||||
{
|
{
|
||||||
IMcpClient client = await CreateMcpClientForServer();
|
await using IMcpClient client = await CreateMcpClientForServer();
|
||||||
|
|
||||||
var prompts = await client.ListPromptsAsync(TestContext.Current.CancellationToken);
|
var prompts = await client.ListPromptsAsync(TestContext.Current.CancellationToken);
|
||||||
Assert.Equal(6, prompts.Count);
|
Assert.Equal(6, prompts.Count);
|
||||||
|
|
@ -166,7 +166,7 @@ public partial class McpServerBuilderExtensionsPromptsTests : ClientServerTestBa
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Throws_When_Prompt_Fails()
|
public async Task Throws_When_Prompt_Fails()
|
||||||
{
|
{
|
||||||
IMcpClient client = await CreateMcpClientForServer();
|
await using IMcpClient client = await CreateMcpClientForServer();
|
||||||
|
|
||||||
await Assert.ThrowsAsync<McpException>(async () => await client.GetPromptAsync(
|
await Assert.ThrowsAsync<McpException>(async () => await client.GetPromptAsync(
|
||||||
nameof(SimplePrompts.ThrowsException),
|
nameof(SimplePrompts.ThrowsException),
|
||||||
|
|
@ -176,7 +176,7 @@ public partial class McpServerBuilderExtensionsPromptsTests : ClientServerTestBa
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Throws_Exception_On_Unknown_Prompt()
|
public async Task Throws_Exception_On_Unknown_Prompt()
|
||||||
{
|
{
|
||||||
IMcpClient client = await CreateMcpClientForServer();
|
await using IMcpClient client = await CreateMcpClientForServer();
|
||||||
|
|
||||||
var e = await Assert.ThrowsAsync<McpException>(async () => await client.GetPromptAsync(
|
var e = await Assert.ThrowsAsync<McpException>(async () => await client.GetPromptAsync(
|
||||||
"NotRegisteredPrompt",
|
"NotRegisteredPrompt",
|
||||||
|
|
@ -188,7 +188,7 @@ public partial class McpServerBuilderExtensionsPromptsTests : ClientServerTestBa
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Throws_Exception_Missing_Parameter()
|
public async Task Throws_Exception_Missing_Parameter()
|
||||||
{
|
{
|
||||||
IMcpClient client = await CreateMcpClientForServer();
|
await using IMcpClient client = await CreateMcpClientForServer();
|
||||||
|
|
||||||
var e = await Assert.ThrowsAsync<McpException>(async () => await client.GetPromptAsync(
|
var e = await Assert.ThrowsAsync<McpException>(async () => await client.GetPromptAsync(
|
||||||
nameof(SimplePrompts.ReturnsChatMessages),
|
nameof(SimplePrompts.ReturnsChatMessages),
|
||||||
|
|
|
||||||
|
|
@ -124,7 +124,7 @@ public partial class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Can_List_Registered_Tools()
|
public async Task Can_List_Registered_Tools()
|
||||||
{
|
{
|
||||||
IMcpClient client = await CreateMcpClientForServer();
|
await using IMcpClient client = await CreateMcpClientForServer();
|
||||||
|
|
||||||
var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
|
var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
|
||||||
Assert.Equal(16, tools.Count);
|
Assert.Equal(16, tools.Count);
|
||||||
|
|
@ -190,7 +190,7 @@ public partial class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Can_Be_Notified_Of_Tool_Changes()
|
public async Task Can_Be_Notified_Of_Tool_Changes()
|
||||||
{
|
{
|
||||||
IMcpClient client = await CreateMcpClientForServer();
|
await using IMcpClient client = await CreateMcpClientForServer();
|
||||||
|
|
||||||
var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
|
var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
|
||||||
Assert.Equal(16, tools.Count);
|
Assert.Equal(16, tools.Count);
|
||||||
|
|
@ -231,7 +231,7 @@ public partial class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Can_Call_Registered_Tool()
|
public async Task Can_Call_Registered_Tool()
|
||||||
{
|
{
|
||||||
IMcpClient client = await CreateMcpClientForServer();
|
await using IMcpClient client = await CreateMcpClientForServer();
|
||||||
|
|
||||||
var result = await client.CallToolAsync(
|
var result = await client.CallToolAsync(
|
||||||
"Echo",
|
"Echo",
|
||||||
|
|
@ -249,7 +249,7 @@ public partial class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Can_Call_Registered_Tool_With_Array_Result()
|
public async Task Can_Call_Registered_Tool_With_Array_Result()
|
||||||
{
|
{
|
||||||
IMcpClient client = await CreateMcpClientForServer();
|
await using IMcpClient client = await CreateMcpClientForServer();
|
||||||
|
|
||||||
var result = await client.CallToolAsync(
|
var result = await client.CallToolAsync(
|
||||||
"EchoArray",
|
"EchoArray",
|
||||||
|
|
@ -273,7 +273,7 @@ public partial class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Can_Call_Registered_Tool_With_Null_Result()
|
public async Task Can_Call_Registered_Tool_With_Null_Result()
|
||||||
{
|
{
|
||||||
IMcpClient client = await CreateMcpClientForServer();
|
await using IMcpClient client = await CreateMcpClientForServer();
|
||||||
|
|
||||||
var result = await client.CallToolAsync(
|
var result = await client.CallToolAsync(
|
||||||
"ReturnNull",
|
"ReturnNull",
|
||||||
|
|
@ -287,7 +287,7 @@ public partial class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Can_Call_Registered_Tool_With_Json_Result()
|
public async Task Can_Call_Registered_Tool_With_Json_Result()
|
||||||
{
|
{
|
||||||
IMcpClient client = await CreateMcpClientForServer();
|
await using IMcpClient client = await CreateMcpClientForServer();
|
||||||
|
|
||||||
var result = await client.CallToolAsync(
|
var result = await client.CallToolAsync(
|
||||||
"ReturnJson",
|
"ReturnJson",
|
||||||
|
|
@ -304,7 +304,7 @@ public partial class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Can_Call_Registered_Tool_With_Int_Result()
|
public async Task Can_Call_Registered_Tool_With_Int_Result()
|
||||||
{
|
{
|
||||||
IMcpClient client = await CreateMcpClientForServer();
|
await using IMcpClient client = await CreateMcpClientForServer();
|
||||||
|
|
||||||
var result = await client.CallToolAsync(
|
var result = await client.CallToolAsync(
|
||||||
"ReturnInteger",
|
"ReturnInteger",
|
||||||
|
|
@ -320,7 +320,7 @@ public partial class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Can_Call_Registered_Tool_And_Pass_ComplexType()
|
public async Task Can_Call_Registered_Tool_And_Pass_ComplexType()
|
||||||
{
|
{
|
||||||
IMcpClient client = await CreateMcpClientForServer();
|
await using IMcpClient client = await CreateMcpClientForServer();
|
||||||
|
|
||||||
var result = await client.CallToolAsync(
|
var result = await client.CallToolAsync(
|
||||||
"EchoComplex",
|
"EchoComplex",
|
||||||
|
|
@ -338,7 +338,7 @@ public partial class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Can_Call_Registered_Tool_With_Instance_Method()
|
public async Task Can_Call_Registered_Tool_With_Instance_Method()
|
||||||
{
|
{
|
||||||
IMcpClient client = await CreateMcpClientForServer();
|
await using IMcpClient client = await CreateMcpClientForServer();
|
||||||
|
|
||||||
string[][] parts = new string[2][];
|
string[][] parts = new string[2][];
|
||||||
for (int i = 0; i < 2; i++)
|
for (int i = 0; i < 2; i++)
|
||||||
|
|
@ -367,7 +367,7 @@ public partial class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Returns_IsError_Content_When_Tool_Fails()
|
public async Task Returns_IsError_Content_When_Tool_Fails()
|
||||||
{
|
{
|
||||||
IMcpClient client = await CreateMcpClientForServer();
|
await using IMcpClient client = await CreateMcpClientForServer();
|
||||||
|
|
||||||
var result = await client.CallToolAsync(
|
var result = await client.CallToolAsync(
|
||||||
"ThrowException",
|
"ThrowException",
|
||||||
|
|
@ -382,7 +382,7 @@ public partial class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Throws_Exception_On_Unknown_Tool()
|
public async Task Throws_Exception_On_Unknown_Tool()
|
||||||
{
|
{
|
||||||
IMcpClient client = await CreateMcpClientForServer();
|
await using IMcpClient client = await CreateMcpClientForServer();
|
||||||
|
|
||||||
var e = await Assert.ThrowsAsync<McpException>(async () => await client.CallToolAsync(
|
var e = await Assert.ThrowsAsync<McpException>(async () => await client.CallToolAsync(
|
||||||
"NotRegisteredTool",
|
"NotRegisteredTool",
|
||||||
|
|
@ -394,7 +394,7 @@ public partial class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Returns_IsError_Missing_Parameter()
|
public async Task Returns_IsError_Missing_Parameter()
|
||||||
{
|
{
|
||||||
IMcpClient client = await CreateMcpClientForServer();
|
await using IMcpClient client = await CreateMcpClientForServer();
|
||||||
|
|
||||||
var result = await client.CallToolAsync(
|
var result = await client.CallToolAsync(
|
||||||
"Echo",
|
"Echo",
|
||||||
|
|
@ -511,7 +511,7 @@ public partial class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Recognizes_Parameter_Types()
|
public async Task Recognizes_Parameter_Types()
|
||||||
{
|
{
|
||||||
IMcpClient client = await CreateMcpClientForServer();
|
await using IMcpClient client = await CreateMcpClientForServer();
|
||||||
|
|
||||||
var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
|
var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
|
|
@ -584,7 +584,7 @@ public partial class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task HandlesIProgressParameter()
|
public async Task HandlesIProgressParameter()
|
||||||
{
|
{
|
||||||
IMcpClient client = await CreateMcpClientForServer();
|
await using IMcpClient client = await CreateMcpClientForServer();
|
||||||
|
|
||||||
var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
|
var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
|
||||||
Assert.NotNull(tools);
|
Assert.NotNull(tools);
|
||||||
|
|
@ -627,7 +627,7 @@ public partial class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task CancellationNotificationsPropagateToToolTokens()
|
public async Task CancellationNotificationsPropagateToToolTokens()
|
||||||
{
|
{
|
||||||
IMcpClient client = await CreateMcpClientForServer();
|
await using IMcpClient client = await CreateMcpClientForServer();
|
||||||
|
|
||||||
var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
|
var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
|
||||||
Assert.NotNull(tools);
|
Assert.NotNull(tools);
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ public partial class McpServerScopedTests : ClientServerTestBase
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task InjectScopedServiceAsArgument()
|
public async Task InjectScopedServiceAsArgument()
|
||||||
{
|
{
|
||||||
IMcpClient client = await CreateMcpClientForServer();
|
await using IMcpClient client = await CreateMcpClientForServer();
|
||||||
|
|
||||||
var tools = await client.ListToolsAsync(McpServerScopedTestsJsonContext.Default.Options, TestContext.Current.CancellationToken);
|
var tools = await client.ListToolsAsync(McpServerScopedTestsJsonContext.Default.Options, TestContext.Current.CancellationToken);
|
||||||
var tool = tools.First(t => t.Name == nameof(EchoTool.EchoComplex));
|
var tool = tools.First(t => t.Name == nameof(EchoTool.EchoComplex));
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ public class CancellationTests : ClientServerTestBase
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task PrecancelRequest_CancelsBeforeSending()
|
public async Task PrecancelRequest_CancelsBeforeSending()
|
||||||
{
|
{
|
||||||
var client = await CreateMcpClientForServer();
|
await using var client = await CreateMcpClientForServer();
|
||||||
|
|
||||||
bool gotCancellation = false;
|
bool gotCancellation = false;
|
||||||
await using (Server.RegisterNotificationHandler(NotificationMethods.CancelledNotification, (notification, cancellationToken) =>
|
await using (Server.RegisterNotificationHandler(NotificationMethods.CancelledNotification, (notification, cancellationToken) =>
|
||||||
|
|
@ -51,7 +51,7 @@ public class CancellationTests : ClientServerTestBase
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task CancellationPropagation_RequestingCancellationCancelsPendingRequest()
|
public async Task CancellationPropagation_RequestingCancellationCancelsPendingRequest()
|
||||||
{
|
{
|
||||||
var client = await CreateMcpClientForServer();
|
await using var client = await CreateMcpClientForServer();
|
||||||
|
|
||||||
var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
|
var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
|
||||||
var waitTool = tools.First(t => t.Name == nameof(WaitForCancellation));
|
var waitTool = tools.First(t => t.Name == nameof(WaitForCancellation));
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ public class NotificationHandlerTests : ClientServerTestBase
|
||||||
public async Task RegistrationsAreRemovedWhenDisposed()
|
public async Task RegistrationsAreRemovedWhenDisposed()
|
||||||
{
|
{
|
||||||
const string NotificationName = "somethingsomething";
|
const string NotificationName = "somethingsomething";
|
||||||
IMcpClient client = await CreateMcpClientForServer();
|
await using IMcpClient client = await CreateMcpClientForServer();
|
||||||
|
|
||||||
const int Iterations = 10;
|
const int Iterations = 10;
|
||||||
|
|
||||||
|
|
@ -40,7 +40,7 @@ public class NotificationHandlerTests : ClientServerTestBase
|
||||||
public async Task MultipleRegistrationsResultInMultipleCallbacks()
|
public async Task MultipleRegistrationsResultInMultipleCallbacks()
|
||||||
{
|
{
|
||||||
const string NotificationName = "somethingsomething";
|
const string NotificationName = "somethingsomething";
|
||||||
IMcpClient client = await CreateMcpClientForServer();
|
await using IMcpClient client = await CreateMcpClientForServer();
|
||||||
|
|
||||||
const int RegistrationCount = 10;
|
const int RegistrationCount = 10;
|
||||||
|
|
||||||
|
|
@ -80,7 +80,7 @@ public class NotificationHandlerTests : ClientServerTestBase
|
||||||
public async Task MultipleHandlersRunEvenIfOneThrows()
|
public async Task MultipleHandlersRunEvenIfOneThrows()
|
||||||
{
|
{
|
||||||
const string NotificationName = "somethingsomething";
|
const string NotificationName = "somethingsomething";
|
||||||
IMcpClient client = await CreateMcpClientForServer();
|
await using IMcpClient client = await CreateMcpClientForServer();
|
||||||
|
|
||||||
const int RegistrationCount = 10;
|
const int RegistrationCount = 10;
|
||||||
|
|
||||||
|
|
@ -122,7 +122,7 @@ public class NotificationHandlerTests : ClientServerTestBase
|
||||||
public async Task DisposeAsyncDoesNotCompleteWhileNotificationHandlerRuns(int numberOfDisposals)
|
public async Task DisposeAsyncDoesNotCompleteWhileNotificationHandlerRuns(int numberOfDisposals)
|
||||||
{
|
{
|
||||||
const string NotificationName = "somethingsomething";
|
const string NotificationName = "somethingsomething";
|
||||||
IMcpClient client = await CreateMcpClientForServer();
|
await using IMcpClient client = await CreateMcpClientForServer();
|
||||||
|
|
||||||
var handlerRunning = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
var handlerRunning = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
var releaseHandler = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
var releaseHandler = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
|
@ -163,7 +163,7 @@ public class NotificationHandlerTests : ClientServerTestBase
|
||||||
public async Task DisposeAsyncCompletesImmediatelyWhenInvokedFromHandler(int numberOfDisposals)
|
public async Task DisposeAsyncCompletesImmediatelyWhenInvokedFromHandler(int numberOfDisposals)
|
||||||
{
|
{
|
||||||
const string NotificationName = "somethingsomething";
|
const string NotificationName = "somethingsomething";
|
||||||
IMcpClient client = await CreateMcpClientForServer();
|
await using IMcpClient client = await CreateMcpClientForServer();
|
||||||
|
|
||||||
var handlerRunning = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
var handlerRunning = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
var releaseHandler = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
var releaseHandler = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue