Skip to content

Commit db8f1a9

Browse files
committed
Change default name casing of McpServerXx.Create tools/prompts
1 parent ab51400 commit db8f1a9

File tree

8 files changed

+94
-39
lines changed

8 files changed

+94
-39
lines changed

src/ModelContextProtocol.Core/Server/AIFunctionMcpServerPrompt.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ private static AIFunctionFactoryOptions CreateAIFunctionFactoryOptions(
6868
MethodInfo method, McpServerPromptCreateOptions? options) =>
6969
new()
7070
{
71-
Name = options?.Name ?? method.GetCustomAttribute<McpServerPromptAttribute>()?.Name,
71+
Name = options?.Name ?? method.GetCustomAttribute<McpServerPromptAttribute>()?.Name ?? AIFunctionMcpServerTool.DeriveName(method, JsonNamingPolicy.CamelCase),
7272
Description = options?.Description,
7373
MarshalResult = static (result, _, cancellationToken) => new ValueTask<object?>(result),
7474
SerializerOptions = options?.SerializerOptions ?? McpJsonUtilities.DefaultOptions,

src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using System.Reflection;
99
using System.Text.Json;
1010
using System.Text.Json.Nodes;
11+
using System.Text.RegularExpressions;
1112

1213
namespace ModelContextProtocol.Server;
1314

@@ -74,7 +75,7 @@ private static AIFunctionFactoryOptions CreateAIFunctionFactoryOptions(
7475
MethodInfo method, McpServerToolCreateOptions? options) =>
7576
new()
7677
{
77-
Name = options?.Name ?? method.GetCustomAttribute<McpServerToolAttribute>()?.Name,
78+
Name = options?.Name ?? method.GetCustomAttribute<McpServerToolAttribute>()?.Name ?? DeriveName(method, JsonNamingPolicy.SnakeCaseLower),
7879
Description = options?.Description,
7980
MarshalResult = static (result, _, cancellationToken) => new ValueTask<object?>(result),
8081
SerializerOptions = options?.SerializerOptions ?? McpJsonUtilities.DefaultOptions,
@@ -293,6 +294,63 @@ public override async ValueTask<CallToolResult> InvokeAsync(
293294
};
294295
}
295296

297+
/// <summary>Creates a name to use based on the supplied method and naming policy.</summary>
298+
internal static string DeriveName(MethodInfo method, JsonNamingPolicy? policy)
299+
{
300+
string name = method.Name;
301+
302+
// Remove any "Async" suffix if the method is an async method and if the method name isn't just "Async".
303+
const string AsyncSuffix = "Async";
304+
if (IsAsyncMethod(method) &&
305+
name.EndsWith(AsyncSuffix, StringComparison.Ordinal) &&
306+
name.Length > AsyncSuffix.Length)
307+
{
308+
name = name.Substring(0, name.Length - AsyncSuffix.Length);
309+
}
310+
311+
// Replace anything other than ASCII letters or digits with underscores, trim off any leading or trailing underscores.
312+
name = NonAsciiLetterDigitsRegex().Replace(name, "_").Trim('_');
313+
314+
// If after all our transformations the name is empty, just use the original method name.
315+
if (name.Length == 0)
316+
{
317+
name = method.Name;
318+
}
319+
320+
// Case the name based on the provided naming policy.
321+
return policy?.ConvertName(name) ?? name;
322+
323+
static bool IsAsyncMethod(MethodInfo method)
324+
{
325+
Type t = method.ReturnType;
326+
327+
if (t == typeof(Task) || t == typeof(ValueTask))
328+
{
329+
return true;
330+
}
331+
332+
if (t.IsGenericType)
333+
{
334+
t = t.GetGenericTypeDefinition();
335+
if (t == typeof(Task<>) || t == typeof(ValueTask<>) || t == typeof(IAsyncEnumerable<>))
336+
{
337+
return true;
338+
}
339+
}
340+
341+
return false;
342+
}
343+
}
344+
345+
/// <summary>Regex that flags runs of characters other than ASCII digits or letters.</summary>
346+
#if NET
347+
[GeneratedRegex("[^0-9A-Za-z]+")]
348+
private static partial Regex NonAsciiLetterDigitsRegex();
349+
#else
350+
private static Regex NonAsciiLetterDigitsRegex() => _nonAsciiLetterDigits;
351+
private static readonly Regex _nonAsciiLetterDigits = new("[^0-9A-Za-z]+", RegexOptions.Compiled);
352+
#endif
353+
296354
private static JsonElement? CreateOutputSchema(AIFunction function, McpServerToolCreateOptions? toolCreateOptions, out bool structuredOutputRequiresWrapping)
297355
{
298356
structuredOutputRequiresWrapping = false;

tests/ModelContextProtocol.AspNetCore.Tests/MapMcpTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ IHttpContextAccessor is not currently supported with non-stateless Streamable HT
8080
await using var mcpClient = await ConnectAsync();
8181

8282
var response = await mcpClient.CallToolAsync(
83-
"EchoWithUserName",
83+
"echo_with_user_name",
8484
new Dictionary<string, object?>() { ["message"] = "Hello world!" },
8585
cancellationToken: TestContext.Current.CancellationToken);
8686

tests/ModelContextProtocol.AspNetCore.Tests/SseIntegrationTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -149,11 +149,11 @@ public async Task AddMcpServer_CanBeCalled_MultipleTimes()
149149
var tools = await mcpClient.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
150150

151151
Assert.Equal(2, tools.Count);
152-
Assert.Contains(tools, tools => tools.Name == "Echo");
152+
Assert.Contains(tools, tools => tools.Name == "echo");
153153
Assert.Contains(tools, tools => tools.Name == "sampleLLM");
154154

155155
var echoResponse = await mcpClient.CallToolAsync(
156-
"Echo",
156+
"echo",
157157
new Dictionary<string, object?>
158158
{
159159
["message"] = "from client!"

tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsPromptsTests.cs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ public async Task Can_List_And_Call_Registered_Prompts()
100100
var prompts = await client.ListPromptsAsync(TestContext.Current.CancellationToken);
101101
Assert.Equal(6, prompts.Count);
102102

103-
var prompt = prompts.First(t => t.Name == nameof(SimplePrompts.ReturnsChatMessages));
103+
var prompt = prompts.First(t => t.Name == "returnsChatMessages");
104104
Assert.Equal("Returns chat messages", prompt.Description);
105105

106106
var result = await prompt.GetAsync(new Dictionary<string, object?>() { ["message"] = "hello" }, cancellationToken: TestContext.Current.CancellationToken);
@@ -171,7 +171,7 @@ public async Task TitleAttributeProperty_PropagatedToTitle()
171171
Assert.NotNull(prompts);
172172
Assert.NotEmpty(prompts);
173173

174-
McpClientPrompt prompt = prompts.First(t => t.Name == nameof(SimplePrompts.ReturnsString));
174+
McpClientPrompt prompt = prompts.First(t => t.Name == "returnsString");
175175

176176
Assert.Equal("This is a title", prompt.Title);
177177
}
@@ -204,7 +204,7 @@ public async Task Throws_Exception_Missing_Parameter()
204204
await using IMcpClient client = await CreateMcpClientForServer();
205205

206206
var e = await Assert.ThrowsAsync<McpException>(async () => await client.GetPromptAsync(
207-
nameof(SimplePrompts.ReturnsChatMessages),
207+
"returnsChatMessages",
208208
cancellationToken: TestContext.Current.CancellationToken));
209209

210210
Assert.Equal(McpErrorCode.InternalError, e.ErrorCode);
@@ -242,7 +242,7 @@ public void Register_Prompts_From_Current_Assembly()
242242
sc.AddMcpServer().WithPromptsFromAssembly();
243243
IServiceProvider services = sc.BuildServiceProvider();
244244

245-
Assert.Contains(services.GetServices<McpServerPrompt>(), t => t.ProtocolPrompt.Name == nameof(SimplePrompts.ReturnsChatMessages));
245+
Assert.Contains(services.GetServices<McpServerPrompt>(), t => t.ProtocolPrompt.Name == "returnsChatMessages");
246246
}
247247

248248
[Fact]
@@ -255,10 +255,10 @@ public void Register_Prompts_From_Multiple_Sources()
255255
.WithPrompts([McpServerPrompt.Create(() => "42", new() { Name = "Returns42" })]);
256256
IServiceProvider services = sc.BuildServiceProvider();
257257

258-
Assert.Contains(services.GetServices<McpServerPrompt>(), t => t.ProtocolPrompt.Name == nameof(SimplePrompts.ReturnsChatMessages));
259-
Assert.Contains(services.GetServices<McpServerPrompt>(), t => t.ProtocolPrompt.Name == nameof(SimplePrompts.ThrowsException));
260-
Assert.Contains(services.GetServices<McpServerPrompt>(), t => t.ProtocolPrompt.Name == nameof(SimplePrompts.ReturnsString));
261-
Assert.Contains(services.GetServices<McpServerPrompt>(), t => t.ProtocolPrompt.Name == nameof(MorePrompts.AnotherPrompt));
258+
Assert.Contains(services.GetServices<McpServerPrompt>(), t => t.ProtocolPrompt.Name == "returnsChatMessages");
259+
Assert.Contains(services.GetServices<McpServerPrompt>(), t => t.ProtocolPrompt.Name == "throwsException");
260+
Assert.Contains(services.GetServices<McpServerPrompt>(), t => t.ProtocolPrompt.Name == "returnsString");
261+
Assert.Contains(services.GetServices<McpServerPrompt>(), t => t.ProtocolPrompt.Name == "anotherPrompt");
262262
Assert.Contains(services.GetServices<McpServerPrompt>(), t => t.ProtocolPrompt.Name == "Returns42");
263263
}
264264

tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsToolsTests.cs

Lines changed: 21 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -126,8 +126,7 @@ public async Task Can_List_Registered_Tools()
126126
var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
127127
Assert.Equal(16, tools.Count);
128128

129-
McpClientTool echoTool = tools.First(t => t.Name == "Echo");
130-
Assert.Equal("Echo", echoTool.Name);
129+
McpClientTool echoTool = tools.First(t => t.Name == "echo");
131130
Assert.Equal("Echoes the input back to the client.", echoTool.Description);
132131
Assert.Equal("object", echoTool.JsonSchema.GetProperty("type").GetString());
133132
Assert.Equal(JsonValueKind.Object, echoTool.JsonSchema.GetProperty("properties").GetProperty("message").ValueKind);
@@ -165,8 +164,7 @@ public async Task Can_Create_Multiple_Servers_From_Options_And_List_Registered_T
165164
var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
166165
Assert.Equal(16, tools.Count);
167166

168-
McpClientTool echoTool = tools.First(t => t.Name == "Echo");
169-
Assert.Equal("Echo", echoTool.Name);
167+
McpClientTool echoTool = tools.First(t => t.Name == "echo");
170168
Assert.Equal("Echoes the input back to the client.", echoTool.Description);
171169
Assert.Equal("object", echoTool.JsonSchema.GetProperty("type").GetString());
172170
Assert.Equal(JsonValueKind.Object, echoTool.JsonSchema.GetProperty("properties").GetProperty("message").ValueKind);
@@ -231,7 +229,7 @@ public async Task Can_Call_Registered_Tool()
231229
await using IMcpClient client = await CreateMcpClientForServer();
232230

233231
var result = await client.CallToolAsync(
234-
"Echo",
232+
"echo",
235233
new Dictionary<string, object?>() { ["message"] = "Peter" },
236234
cancellationToken: TestContext.Current.CancellationToken);
237235

@@ -250,7 +248,7 @@ public async Task Can_Call_Registered_Tool_With_Array_Result()
250248
await using IMcpClient client = await CreateMcpClientForServer();
251249

252250
var result = await client.CallToolAsync(
253-
"EchoArray",
251+
"echo_array",
254252
new Dictionary<string, object?>() { ["message"] = "Peter" },
255253
cancellationToken: TestContext.Current.CancellationToken);
256254

@@ -274,7 +272,7 @@ public async Task Can_Call_Registered_Tool_With_Null_Result()
274272
await using IMcpClient client = await CreateMcpClientForServer();
275273

276274
var result = await client.CallToolAsync(
277-
"ReturnNull",
275+
"return_null",
278276
cancellationToken: TestContext.Current.CancellationToken);
279277

280278
Assert.NotNull(result);
@@ -288,7 +286,7 @@ public async Task Can_Call_Registered_Tool_With_Json_Result()
288286
await using IMcpClient client = await CreateMcpClientForServer();
289287

290288
var result = await client.CallToolAsync(
291-
"ReturnJson",
289+
"return_json",
292290
cancellationToken: TestContext.Current.CancellationToken);
293291

294292
Assert.NotNull(result);
@@ -305,7 +303,7 @@ public async Task Can_Call_Registered_Tool_With_Int_Result()
305303
await using IMcpClient client = await CreateMcpClientForServer();
306304

307305
var result = await client.CallToolAsync(
308-
"ReturnInteger",
306+
"return_integer",
309307
cancellationToken: TestContext.Current.CancellationToken);
310308

311309
Assert.NotNull(result.Content);
@@ -320,7 +318,7 @@ public async Task Can_Call_Registered_Tool_And_Pass_ComplexType()
320318
await using IMcpClient client = await CreateMcpClientForServer();
321319

322320
var result = await client.CallToolAsync(
323-
"EchoComplex",
321+
"echo_complex",
324322
new Dictionary<string, object?>() { ["complex"] = JsonDocument.Parse("""{"Name": "Peter", "Age": 25}""").RootElement },
325323
cancellationToken: TestContext.Current.CancellationToken);
326324

@@ -340,7 +338,7 @@ public async Task Can_Call_Registered_Tool_With_Instance_Method()
340338
for (int i = 0; i < 2; i++)
341339
{
342340
var result = await client.CallToolAsync(
343-
nameof(EchoTool.GetCtorParameter),
341+
"get_ctor_parameter",
344342
cancellationToken: TestContext.Current.CancellationToken);
345343

346344
Assert.NotNull(result);
@@ -366,7 +364,7 @@ public async Task Returns_IsError_Content_When_Tool_Fails()
366364
await using IMcpClient client = await CreateMcpClientForServer();
367365

368366
var result = await client.CallToolAsync(
369-
"ThrowException",
367+
"throw_exception",
370368
cancellationToken: TestContext.Current.CancellationToken);
371369

372370
Assert.True(result.IsError);
@@ -393,7 +391,7 @@ public async Task Returns_IsError_Missing_Parameter()
393391
await using IMcpClient client = await CreateMcpClientForServer();
394392

395393
var result = await client.CallToolAsync(
396-
"Echo",
394+
"echo",
397395
cancellationToken: TestContext.Current.CancellationToken);
398396

399397
Assert.True(result.IsError);
@@ -436,7 +434,7 @@ public void Register_Tools_From_Current_Assembly()
436434
sc.AddMcpServer().WithToolsFromAssembly();
437435
IServiceProvider services = sc.BuildServiceProvider();
438436

439-
Assert.Contains(services.GetServices<McpServerTool>(), t => t.ProtocolTool.Name == "Echo");
437+
Assert.Contains(services.GetServices<McpServerTool>(), t => t.ProtocolTool.Name == "echo");
440438
}
441439

442440
[Theory]
@@ -452,7 +450,7 @@ public void WithTools_Parameters_Satisfiable_From_DI(bool parameterInServices)
452450
sc.AddMcpServer().WithTools([typeof(EchoTool)], BuilderToolsJsonContext.Default.Options);
453451
IServiceProvider services = sc.BuildServiceProvider();
454452

455-
McpServerTool tool = services.GetServices<McpServerTool>().First(t => t.ProtocolTool.Name == "EchoComplex");
453+
McpServerTool tool = services.GetServices<McpServerTool>().First(t => t.ProtocolTool.Name == "echo_complex");
456454
if (parameterInServices)
457455
{
458456
Assert.DoesNotContain("\"complex\"", JsonSerializer.Serialize(tool.ProtocolTool.InputSchema, AIJsonUtilities.DefaultOptions));
@@ -495,7 +493,7 @@ public void WithToolsFromAssembly_Parameters_Satisfiable_From_DI(ServiceLifetime
495493
sc.AddMcpServer().WithToolsFromAssembly();
496494
IServiceProvider services = sc.BuildServiceProvider();
497495

498-
McpServerTool tool = services.GetServices<McpServerTool>().First(t => t.ProtocolTool.Name == "EchoComplex");
496+
McpServerTool tool = services.GetServices<McpServerTool>().First(t => t.ProtocolTool.Name == "echo_complex");
499497
if (lifetime is not null)
500498
{
501499
Assert.DoesNotContain("\"complex\"", JsonSerializer.Serialize(tool.ProtocolTool.InputSchema, AIJsonUtilities.DefaultOptions));
@@ -516,8 +514,7 @@ public async Task Recognizes_Parameter_Types()
516514
Assert.NotNull(tools);
517515
Assert.NotEmpty(tools);
518516

519-
var tool = tools.First(t => t.Name == "TestTool");
520-
Assert.Equal("TestTool", tool.Name);
517+
var tool = tools.First(t => t.Name == "test_tool");
521518
Assert.Empty(tool.Description!);
522519
Assert.Equal("object", tool.JsonSchema.GetProperty("type").GetString());
523520

@@ -543,9 +540,9 @@ public void Register_Tools_From_Multiple_Sources()
543540

544541
Assert.Contains(services.GetServices<McpServerTool>(), t => t.ProtocolTool.Name == "double_echo");
545542
Assert.Contains(services.GetServices<McpServerTool>(), t => t.ProtocolTool.Name == "DifferentName");
546-
Assert.Contains(services.GetServices<McpServerTool>(), t => t.ProtocolTool.Name == "MethodB");
547-
Assert.Contains(services.GetServices<McpServerTool>(), t => t.ProtocolTool.Name == "MethodC");
548-
Assert.Contains(services.GetServices<McpServerTool>(), t => t.ProtocolTool.Name == "MethodD");
543+
Assert.Contains(services.GetServices<McpServerTool>(), t => t.ProtocolTool.Name == "method_b");
544+
Assert.Contains(services.GetServices<McpServerTool>(), t => t.ProtocolTool.Name == "method_c");
545+
Assert.Contains(services.GetServices<McpServerTool>(), t => t.ProtocolTool.Name == "method_d");
549546
Assert.Contains(services.GetServices<McpServerTool>(), t => t.ProtocolTool.Name == "Returns42");
550547
}
551548

@@ -591,7 +588,7 @@ public async Task TitleAttributeProperty_PropagatedToTitle()
591588
Assert.NotNull(tools);
592589
Assert.NotEmpty(tools);
593590

594-
McpClientTool tool = tools.First(t => t.Name == nameof(EchoTool.EchoComplex));
591+
McpClientTool tool = tools.First(t => t.Name == "echo_complex");
595592

596593
Assert.Equal("This is a title", tool.Title);
597594
Assert.Equal("This is a title", tool.ProtocolTool.Title);
@@ -607,7 +604,7 @@ public async Task HandlesIProgressParameter()
607604
Assert.NotNull(tools);
608605
Assert.NotEmpty(tools);
609606

610-
McpClientTool progressTool = tools.First(t => t.Name == nameof(EchoTool.SendsProgressNotifications));
607+
McpClientTool progressTool = tools.First(t => t.Name == "sends_progress_notifications");
611608

612609
TaskCompletionSource tcs = new(TaskCreationOptions.RunContinuationsAsynchronously);
613610
int remainingNotifications = 10;
@@ -660,7 +657,7 @@ public async Task CancellationNotificationsPropagateToToolTokens()
660657
var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
661658
Assert.NotNull(tools);
662659
Assert.NotEmpty(tools);
663-
McpClientTool cancelableTool = tools.First(t => t.Name == nameof(EchoTool.InfiniteCancelableOperation));
660+
McpClientTool cancelableTool = tools.First(t => t.Name == "infinite_cancelable_operation");
664661

665662
var requestId = new RequestId(Guid.NewGuid().ToString());
666663
var invokeTask = client.SendRequestAsync<CallToolRequestParams, CallToolResult>(

tests/ModelContextProtocol.Tests/Configuration/McpServerScopedTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public async Task InjectScopedServiceAsArgument()
2525
await using IMcpClient client = await CreateMcpClientForServer();
2626

2727
var tools = await client.ListToolsAsync(McpServerScopedTestsJsonContext.Default.Options, TestContext.Current.CancellationToken);
28-
var tool = tools.First(t => t.Name == nameof(EchoTool.EchoComplex));
28+
var tool = tools.First(t => t.Name == "echo_complex");
2929
Assert.DoesNotContain("\"complex\"", JsonSerializer.Serialize(tool.JsonSchema, McpJsonUtilities.DefaultOptions));
3030

3131
int startingConstructed = ComplexObject.Constructed;

tests/ModelContextProtocol.Tests/Protocol/CancellationTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ public async Task CancellationPropagation_RequestingCancellationCancelsPendingRe
5454
await using var client = await CreateMcpClientForServer();
5555

5656
var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
57-
var waitTool = tools.First(t => t.Name == nameof(WaitForCancellation));
57+
var waitTool = tools.First(t => t.Name == "wait_for_cancellation");
5858

5959
CancellationTokenSource cts = new();
6060
var waitTask = waitTool.InvokeAsync(cancellationToken: cts.Token);

0 commit comments

Comments
 (0)