Change default name casing of McpServerXx.Create tools/prompts/resources (#568)

This commit is contained in:
Stephen Toub 2025-07-01 16:40:42 -04:00 • committed by GitHub
commit 4411f1ce5e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 104 additions and 49 deletions

View file

@ -68,7 +68,7 @@ internal sealed class AIFunctionMcpServerPrompt : McpServerPrompt
MethodInfo method, McpServerPromptCreateOptions? options) =>
new()
{
Name = options?.Name ?? method.GetCustomAttribute<McpServerPromptAttribute>()?.Name,
Name = options?.Name ?? method.GetCustomAttribute<McpServerPromptAttribute>()?.Name ?? AIFunctionMcpServerTool.DeriveName(method),
Description = options?.Description,
MarshalResult = static (result, _, cancellationToken) => new ValueTask<object?>(result),
SerializerOptions = options?.SerializerOptions ?? McpJsonUtilities.DefaultOptions,

View file

@ -75,7 +75,7 @@ internal sealed class AIFunctionMcpServerResource : McpServerResource
MethodInfo method, McpServerResourceCreateOptions? options) =>
new()
{
Name = options?.Name ?? method.GetCustomAttribute<McpServerResourceAttribute>()?.Name,
Name = options?.Name ?? method.GetCustomAttribute<McpServerResourceAttribute>()?.Name ?? AIFunctionMcpServerTool.DeriveName(method),
Description = options?.Description,
MarshalResult = static (result, _, cancellationToken) => new ValueTask<object?>(result),
SerializerOptions = options?.SerializerOptions ?? McpJsonUtilities.DefaultOptions,

View file

@ -8,6 +8,7 @@ using System.Diagnostics;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.RegularExpressions;
namespace ModelContextProtocol.Server;
@ -74,7 +75,7 @@ internal sealed partial class AIFunctionMcpServerTool : McpServerTool
MethodInfo method, McpServerToolCreateOptions? options) =>
new()
{
Name = options?.Name ?? method.GetCustomAttribute<McpServerToolAttribute>()?.Name,
Name = options?.Name ?? method.GetCustomAttribute<McpServerToolAttribute>()?.Name ?? DeriveName(method),
Description = options?.Description,
MarshalResult = static (result, _, cancellationToken) => new ValueTask<object?>(result),
SerializerOptions = options?.SerializerOptions ?? McpJsonUtilities.DefaultOptions,
@ -293,6 +294,63 @@ internal sealed partial class AIFunctionMcpServerTool : McpServerTool
};
}
/// <summary>Creates a name to use based on the supplied method and naming policy.</summary>
internal static string DeriveName(MethodInfo method, JsonNamingPolicy? policy = null)
{
string name = method.Name;
// Remove any "Async" suffix if the method is an async method and if the method name isn't just "Async".
const string AsyncSuffix = "Async";
if (IsAsyncMethod(method) &&
name.EndsWith(AsyncSuffix, StringComparison.Ordinal) &&
name.Length > AsyncSuffix.Length)
{
name = name.Substring(0, name.Length - AsyncSuffix.Length);
}
// Replace anything other than ASCII letters or digits with underscores, trim off any leading or trailing underscores.
name = NonAsciiLetterDigitsRegex().Replace(name, "_").Trim('_');
// If after all our transformations the name is empty, just use the original method name.
if (name.Length == 0)
{
name = method.Name;
}
// Case the name based on the provided naming policy.
return (policy ?? JsonNamingPolicy.SnakeCaseLower).ConvertName(name) ?? name;
static bool IsAsyncMethod(MethodInfo method)
{
Type t = method.ReturnType;
if (t == typeof(Task) || t == typeof(ValueTask))
{
return true;
}
if (t.IsGenericType)
{
t = t.GetGenericTypeDefinition();
if (t == typeof(Task<>) || t == typeof(ValueTask<>) || t == typeof(IAsyncEnumerable<>))
{
return true;
}
}
return false;
}
}
/// <summary>Regex that flags runs of characters other than ASCII digits or letters.</summary>
#if NET
[GeneratedRegex("[^0-9A-Za-z]+")]
private static partial Regex NonAsciiLetterDigitsRegex();
#else
private static Regex NonAsciiLetterDigitsRegex() => _nonAsciiLetterDigits;
private static readonly Regex _nonAsciiLetterDigits = new("[^0-9A-Za-z]+", RegexOptions.Compiled);
#endif
private static JsonElement? CreateOutputSchema(AIFunction function, McpServerToolCreateOptions? toolCreateOptions, out bool structuredOutputRequiresWrapping)
{
structuredOutputRequiresWrapping = false;

View file

@ -80,7 +80,7 @@ public abstract class MapMcpTests(ITestOutputHelper testOutputHelper) : KestrelI
await using var mcpClient = await ConnectAsync();
var response = await mcpClient.CallToolAsync(
"EchoWithUserName",
"echo_with_user_name",
new Dictionary<string, object?>() { ["message"] = "Hello world!" },
cancellationToken: TestContext.Current.CancellationToken);

View file

@ -149,11 +149,11 @@ public partial class SseIntegrationTests(ITestOutputHelper outputHelper) : Kestr
var tools = await mcpClient.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
Assert.Equal(2, tools.Count);
Assert.Contains(tools, tools => tools.Name == "Echo");
Assert.Contains(tools, tools => tools.Name == "echo");
Assert.Contains(tools, tools => tools.Name == "sampleLLM");
var echoResponse = await mcpClient.CallToolAsync(
"Echo",
"echo",
new Dictionary<string, object?>
{
["message"] = "from client!"

View file

@ -100,7 +100,7 @@ public partial class McpServerBuilderExtensionsPromptsTests : ClientServerTestBa
var prompts = await client.ListPromptsAsync(TestContext.Current.CancellationToken);
Assert.Equal(6, prompts.Count);
var prompt = prompts.First(t => t.Name == nameof(SimplePrompts.ReturnsChatMessages));
var prompt = prompts.First(t => t.Name == "returns_chat_messages");
Assert.Equal("Returns chat messages", prompt.Description);
var result = await prompt.GetAsync(new Dictionary<string, object?>() { ["message"] = "hello" }, cancellationToken: TestContext.Current.CancellationToken);
@ -171,7 +171,7 @@ public partial class McpServerBuilderExtensionsPromptsTests : ClientServerTestBa
Assert.NotNull(prompts);
Assert.NotEmpty(prompts);
McpClientPrompt prompt = prompts.First(t => t.Name == nameof(SimplePrompts.ReturnsString));
McpClientPrompt prompt = prompts.First(t => t.Name == "returns_string");
Assert.Equal("This is a title", prompt.Title);
}
@ -204,7 +204,7 @@ public partial class McpServerBuilderExtensionsPromptsTests : ClientServerTestBa
await using IMcpClient client = await CreateMcpClientForServer();
var e = await Assert.ThrowsAsync<McpException>(async () => await client.GetPromptAsync(
nameof(SimplePrompts.ReturnsChatMessages),
"returns_chat_messages",
cancellationToken: TestContext.Current.CancellationToken));
Assert.Equal(McpErrorCode.InternalError, e.ErrorCode);
@ -242,7 +242,7 @@ public partial class McpServerBuilderExtensionsPromptsTests : ClientServerTestBa
sc.AddMcpServer().WithPromptsFromAssembly();
IServiceProvider services = sc.BuildServiceProvider();
Assert.Contains(services.GetServices<McpServerPrompt>(), t => t.ProtocolPrompt.Name == nameof(SimplePrompts.ReturnsChatMessages));
Assert.Contains(services.GetServices<McpServerPrompt>(), t => t.ProtocolPrompt.Name == "returns_chat_messages");
}
[Fact]
@ -255,10 +255,10 @@ public partial class McpServerBuilderExtensionsPromptsTests : ClientServerTestBa
.WithPrompts([McpServerPrompt.Create(() => "42", new() { Name = "Returns42" })]);
IServiceProvider services = sc.BuildServiceProvider();
Assert.Contains(services.GetServices<McpServerPrompt>(), t => t.ProtocolPrompt.Name == nameof(SimplePrompts.ReturnsChatMessages));
Assert.Contains(services.GetServices<McpServerPrompt>(), t => t.ProtocolPrompt.Name == nameof(SimplePrompts.ThrowsException));
Assert.Contains(services.GetServices<McpServerPrompt>(), t => t.ProtocolPrompt.Name == nameof(SimplePrompts.ReturnsString));
Assert.Contains(services.GetServices<McpServerPrompt>(), t => t.ProtocolPrompt.Name == nameof(MorePrompts.AnotherPrompt));
Assert.Contains(services.GetServices<McpServerPrompt>(), t => t.ProtocolPrompt.Name == "returns_chat_messages");
Assert.Contains(services.GetServices<McpServerPrompt>(), t => t.ProtocolPrompt.Name == "throws_exception");
Assert.Contains(services.GetServices<McpServerPrompt>(), t => t.ProtocolPrompt.Name == "returns_string");
Assert.Contains(services.GetServices<McpServerPrompt>(), t => t.ProtocolPrompt.Name == "another_prompt");
Assert.Contains(services.GetServices<McpServerPrompt>(), t => t.ProtocolPrompt.Name == "Returns42");
}

View file

@ -129,7 +129,7 @@ public partial class McpServerBuilderExtensionsResourcesTests : ClientServerTest
var resources = await client.ListResourcesAsync(TestContext.Current.CancellationToken);
Assert.Equal(5, resources.Count);
var resource = resources.First(t => t.Name == nameof(SimpleResources.SomeNeatDirectResource));
var resource = resources.First(t => t.Name == "some_neat_direct_resource");
Assert.Equal("Some neat direct resource", resource.Description);
var result = await resource.ReadAsync(cancellationToken: TestContext.Current.CancellationToken);
@ -146,7 +146,7 @@ public partial class McpServerBuilderExtensionsResourcesTests : ClientServerTest
var resources = await client.ListResourceTemplatesAsync(TestContext.Current.CancellationToken);
Assert.Equal(3, resources.Count);
var resource = resources.First(t => t.Name == nameof(SimpleResources.SomeNeatTemplatedResource));
var resource = resources.First(t => t.Name == "some_neat_templated_resource");
Assert.Equal("Some neat resource with parameters", resource.Description);
var result = await resource.ReadAsync(new Dictionary<string, object?>() { ["name"] = "hello" }, cancellationToken: TestContext.Current.CancellationToken);
@ -204,13 +204,13 @@ public partial class McpServerBuilderExtensionsResourcesTests : ClientServerTest
var resources = await client.ListResourcesAsync(cancellationToken: TestContext.Current.CancellationToken);
Assert.NotNull(resources);
Assert.NotEmpty(resources);
McpClientResource resource = resources.First(t => t.Name == nameof(SimpleResources.SomeNeatDirectResource));
McpClientResource resource = resources.First(t => t.Name == "some_neat_direct_resource");
Assert.Equal("This is a title", resource.Title);
var resourceTemplates = await client.ListResourceTemplatesAsync(cancellationToken: TestContext.Current.CancellationToken);
Assert.NotNull(resourceTemplates);
Assert.NotEmpty(resourceTemplates);
McpClientResourceTemplate resourceTemplate = resourceTemplates.First(t => t.Name == nameof(SimpleResources.SomeNeatTemplatedResource));
McpClientResourceTemplate resourceTemplate = resourceTemplates.First(t => t.Name == "some_neat_templated_resource");
Assert.Equal("This is another title", resourceTemplate.Title);
}
@ -268,8 +268,8 @@ public partial class McpServerBuilderExtensionsResourcesTests : ClientServerTest
sc.AddMcpServer().WithResourcesFromAssembly();
IServiceProvider services = sc.BuildServiceProvider();
Assert.Contains(services.GetServices<McpServerResource>(), t => t.ProtocolResource?.Uri == $"resource://mcp/{nameof(SimpleResources.SomeNeatDirectResource)}");
Assert.Contains(services.GetServices<McpServerResource>(), t => t.ProtocolResourceTemplate?.UriTemplate == $"resource://mcp/{nameof(SimpleResources.SomeNeatTemplatedResource)}{{?name}}");
Assert.Contains(services.GetServices<McpServerResource>(), t => t.ProtocolResource?.Uri == $"resource://mcp/some_neat_direct_resource");
Assert.Contains(services.GetServices<McpServerResource>(), t => t.ProtocolResourceTemplate?.UriTemplate == $"resource://mcp/some_neat_templated_resource{{?name}}");
}
[Fact]
@ -282,9 +282,9 @@ public partial class McpServerBuilderExtensionsResourcesTests : ClientServerTest
.WithResources([McpServerResource.Create(() => "42", new() { UriTemplate = "myResources:///returns42/{something}" })]);
IServiceProvider services = sc.BuildServiceProvider();
Assert.Contains(services.GetServices<McpServerResource>(), t => t.ProtocolResource?.Uri == $"resource://mcp/{nameof(SimpleResources.SomeNeatDirectResource)}");
Assert.Contains(services.GetServices<McpServerResource>(), t => t.ProtocolResourceTemplate?.UriTemplate == $"resource://mcp/{nameof(SimpleResources.SomeNeatTemplatedResource)}{{?name}}");
Assert.Contains(services.GetServices<McpServerResource>(), t => t.ProtocolResourceTemplate?.UriTemplate == $"resource://mcp/{nameof(MoreResources.AnotherNeatDirectResource)}");
Assert.Contains(services.GetServices<McpServerResource>(), t => t.ProtocolResource?.Uri == $"resource://mcp/some_neat_direct_resource");
Assert.Contains(services.GetServices<McpServerResource>(), t => t.ProtocolResourceTemplate?.UriTemplate == $"resource://mcp/some_neat_templated_resource{{?name}}");
Assert.Contains(services.GetServices<McpServerResource>(), t => t.ProtocolResourceTemplate?.UriTemplate == $"resource://mcp/another_neat_direct_resource");
Assert.Contains(services.GetServices<McpServerResource>(), t => t.ProtocolResourceTemplate.UriTemplate == "myResources:///returns42/{something}");
}

View file

@ -126,8 +126,7 @@ public partial class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
Assert.Equal(16, tools.Count);
McpClientTool echoTool = tools.First(t => t.Name == "Echo");
Assert.Equal("Echo", echoTool.Name);
McpClientTool echoTool = tools.First(t => t.Name == "echo");
Assert.Equal("Echoes the input back to the client.", echoTool.Description);
Assert.Equal("object", echoTool.JsonSchema.GetProperty("type").GetString());
Assert.Equal(JsonValueKind.Object, echoTool.JsonSchema.GetProperty("properties").GetProperty("message").ValueKind);
@ -165,8 +164,7 @@ public partial class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
Assert.Equal(16, tools.Count);
McpClientTool echoTool = tools.First(t => t.Name == "Echo");
Assert.Equal("Echo", echoTool.Name);
McpClientTool echoTool = tools.First(t => t.Name == "echo");
Assert.Equal("Echoes the input back to the client.", echoTool.Description);
Assert.Equal("object", echoTool.JsonSchema.GetProperty("type").GetString());
Assert.Equal(JsonValueKind.Object, echoTool.JsonSchema.GetProperty("properties").GetProperty("message").ValueKind);
@ -231,7 +229,7 @@ public partial class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
await using IMcpClient client = await CreateMcpClientForServer();
var result = await client.CallToolAsync(
"Echo",
"echo",
new Dictionary<string, object?>() { ["message"] = "Peter" },
cancellationToken: TestContext.Current.CancellationToken);
@ -250,7 +248,7 @@ public partial class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
await using IMcpClient client = await CreateMcpClientForServer();
var result = await client.CallToolAsync(
"EchoArray",
"echo_array",
new Dictionary<string, object?>() { ["message"] = "Peter" },
cancellationToken: TestContext.Current.CancellationToken);
@ -274,7 +272,7 @@ public partial class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
await using IMcpClient client = await CreateMcpClientForServer();
var result = await client.CallToolAsync(
"ReturnNull",
"return_null",
cancellationToken: TestContext.Current.CancellationToken);
Assert.NotNull(result);
@ -288,7 +286,7 @@ public partial class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
await using IMcpClient client = await CreateMcpClientForServer();
var result = await client.CallToolAsync(
"ReturnJson",
"return_json",
cancellationToken: TestContext.Current.CancellationToken);
Assert.NotNull(result);
@ -305,7 +303,7 @@ public partial class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
await using IMcpClient client = await CreateMcpClientForServer();
var result = await client.CallToolAsync(
"ReturnInteger",
"return_integer",
cancellationToken: TestContext.Current.CancellationToken);
Assert.NotNull(result.Content);
@ -320,7 +318,7 @@ public partial class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
await using IMcpClient client = await CreateMcpClientForServer();
var result = await client.CallToolAsync(
"EchoComplex",
"echo_complex",
new Dictionary<string, object?>() { ["complex"] = JsonDocument.Parse("""{"Name": "Peter", "Age": 25}""").RootElement },
cancellationToken: TestContext.Current.CancellationToken);
@ -340,7 +338,7 @@ public partial class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
for (int i = 0; i < 2; i++)
{
var result = await client.CallToolAsync(
nameof(EchoTool.GetCtorParameter),
"get_ctor_parameter",
cancellationToken: TestContext.Current.CancellationToken);
Assert.NotNull(result);
@ -366,7 +364,7 @@ public partial class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
await using IMcpClient client = await CreateMcpClientForServer();
var result = await client.CallToolAsync(
"ThrowException",
"throw_exception",
cancellationToken: TestContext.Current.CancellationToken);
Assert.True(result.IsError);
@ -393,7 +391,7 @@ public partial class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
await using IMcpClient client = await CreateMcpClientForServer();
var result = await client.CallToolAsync(
"Echo",
"echo",
cancellationToken: TestContext.Current.CancellationToken);
Assert.True(result.IsError);
@ -436,7 +434,7 @@ public partial class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
sc.AddMcpServer().WithToolsFromAssembly();
IServiceProvider services = sc.BuildServiceProvider();
Assert.Contains(services.GetServices<McpServerTool>(), t => t.ProtocolTool.Name == "Echo");
Assert.Contains(services.GetServices<McpServerTool>(), t => t.ProtocolTool.Name == "echo");
}
[Theory]
@ -452,7 +450,7 @@ public partial class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
sc.AddMcpServer().WithTools([typeof(EchoTool)], BuilderToolsJsonContext.Default.Options);
IServiceProvider services = sc.BuildServiceProvider();
McpServerTool tool = services.GetServices<McpServerTool>().First(t => t.ProtocolTool.Name == "EchoComplex");
McpServerTool tool = services.GetServices<McpServerTool>().First(t => t.ProtocolTool.Name == "echo_complex");
if (parameterInServices)
{
Assert.DoesNotContain("\"complex\"", JsonSerializer.Serialize(tool.ProtocolTool.InputSchema, AIJsonUtilities.DefaultOptions));
@ -495,7 +493,7 @@ public partial class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
sc.AddMcpServer().WithToolsFromAssembly();
IServiceProvider services = sc.BuildServiceProvider();
McpServerTool tool = services.GetServices<McpServerTool>().First(t => t.ProtocolTool.Name == "EchoComplex");
McpServerTool tool = services.GetServices<McpServerTool>().First(t => t.ProtocolTool.Name == "echo_complex");
if (lifetime is not null)
{
Assert.DoesNotContain("\"complex\"", JsonSerializer.Serialize(tool.ProtocolTool.InputSchema, AIJsonUtilities.DefaultOptions));
@ -516,8 +514,7 @@ public partial class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
Assert.NotNull(tools);
Assert.NotEmpty(tools);
var tool = tools.First(t => t.Name == "TestTool");
Assert.Equal("TestTool", tool.Name);
var tool = tools.First(t => t.Name == "test_tool");
Assert.Empty(tool.Description!);
Assert.Equal("object", tool.JsonSchema.GetProperty("type").GetString());
@ -543,9 +540,9 @@ public partial class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
Assert.Contains(services.GetServices<McpServerTool>(), t => t.ProtocolTool.Name == "double_echo");
Assert.Contains(services.GetServices<McpServerTool>(), t => t.ProtocolTool.Name == "DifferentName");
Assert.Contains(services.GetServices<McpServerTool>(), t => t.ProtocolTool.Name == "MethodB");
Assert.Contains(services.GetServices<McpServerTool>(), t => t.ProtocolTool.Name == "MethodC");
Assert.Contains(services.GetServices<McpServerTool>(), t => t.ProtocolTool.Name == "MethodD");
Assert.Contains(services.GetServices<McpServerTool>(), t => t.ProtocolTool.Name == "method_b");
Assert.Contains(services.GetServices<McpServerTool>(), t => t.ProtocolTool.Name == "method_c");
Assert.Contains(services.GetServices<McpServerTool>(), t => t.ProtocolTool.Name == "method_d");
Assert.Contains(services.GetServices<McpServerTool>(), t => t.ProtocolTool.Name == "Returns42");
}
@ -591,7 +588,7 @@ public partial class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
Assert.NotNull(tools);
Assert.NotEmpty(tools);
McpClientTool tool = tools.First(t => t.Name == nameof(EchoTool.EchoComplex));
McpClientTool tool = tools.First(t => t.Name == "echo_complex");
Assert.Equal("This is a title", tool.Title);
Assert.Equal("This is a title", tool.ProtocolTool.Title);
@ -607,7 +604,7 @@ public partial class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
Assert.NotNull(tools);
Assert.NotEmpty(tools);
McpClientTool progressTool = tools.First(t => t.Name == nameof(EchoTool.SendsProgressNotifications));
McpClientTool progressTool = tools.First(t => t.Name == "sends_progress_notifications");
TaskCompletionSource tcs = new(TaskCreationOptions.RunContinuationsAsynchronously);
int remainingNotifications = 10;
@ -660,7 +657,7 @@ public partial class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
Assert.NotNull(tools);
Assert.NotEmpty(tools);
McpClientTool cancelableTool = tools.First(t => t.Name == nameof(EchoTool.InfiniteCancelableOperation));
McpClientTool cancelableTool = tools.First(t => t.Name == "infinite_cancelable_operation");
var requestId = new RequestId(Guid.NewGuid().ToString());
var invokeTask = client.SendRequestAsync<CallToolRequestParams, CallToolResult>(

View file

@ -25,7 +25,7 @@ public partial class McpServerScopedTests : ClientServerTestBase
await using IMcpClient client = await CreateMcpClientForServer();
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 == "echo_complex");
Assert.DoesNotContain("\"complex\"", JsonSerializer.Serialize(tool.JsonSchema, McpJsonUtilities.DefaultOptions));
int startingConstructed = ComplexObject.Constructed;

View file

@ -54,7 +54,7 @@ public class CancellationTests : ClientServerTestBase
await using var client = await CreateMcpClientForServer();
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 == "wait_for_cancellation");
CancellationTokenSource cts = new();
var waitTask = waitTool.InvokeAsync(cancellationToken: cts.Token);