Skip to content

Commit 4411f1c

Browse files
authored
Change default name casing of McpServerXx.Create tools/prompts/resources (#568)
1 parent 4e290f8 commit 4411f1c

File tree

10 files changed

+104
-49
lines changed

10 files changed

+104
-49
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),
7272
Description = options?.Description,
7373
MarshalResult = static (result, _, cancellationToken) => new ValueTask<object?>(result),
7474
SerializerOptions = options?.SerializerOptions ?? McpJsonUtilities.DefaultOptions,

src/ModelContextProtocol.Core/Server/AIFunctionMcpServerResource.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ private static AIFunctionFactoryOptions CreateAIFunctionFactoryOptions(
7575
MethodInfo method, McpServerResourceCreateOptions? options) =>
7676
new()
7777
{
78-
Name = options?.Name ?? method.GetCustomAttribute<McpServerResourceAttribute>()?.Name,
78+
Name = options?.Name ?? method.GetCustomAttribute<McpServerResourceAttribute>()?.Name ?? AIFunctionMcpServerTool.DeriveName(method),
7979
Description = options?.Description,
8080
MarshalResult = static (result, _, cancellationToken) => new ValueTask<object?>(result),
8181
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),
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 = null)
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 ?? JsonNamingPolicy.SnakeCaseLower).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 == "returns_chat_messages");
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 == "returns_string");
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+
"returns_chat_messages",
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 == "returns_chat_messages");
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 == "returns_chat_messages");
259+
Assert.Contains(services.GetServices<McpServerPrompt>(), t => t.ProtocolPrompt.Name == "throws_exception");
260+
Assert.Contains(services.GetServices<McpServerPrompt>(), t => t.ProtocolPrompt.Name == "returns_string");
261+
Assert.Contains(services.GetServices<McpServerPrompt>(), t => t.ProtocolPrompt.Name == "another_prompt");
262262
Assert.Contains(services.GetServices<McpServerPrompt>(), t => t.ProtocolPrompt.Name == "Returns42");
263263
}
264264

tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsResourcesTests.cs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ public async Task Can_List_And_Call_Registered_Resources()
129129
var resources = await client.ListResourcesAsync(TestContext.Current.CancellationToken);
130130
Assert.Equal(5, resources.Count);
131131

132-
var resource = resources.First(t => t.Name == nameof(SimpleResources.SomeNeatDirectResource));
132+
var resource = resources.First(t => t.Name == "some_neat_direct_resource");
133133
Assert.Equal("Some neat direct resource", resource.Description);
134134

135135
var result = await resource.ReadAsync(cancellationToken: TestContext.Current.CancellationToken);
@@ -146,7 +146,7 @@ public async Task Can_List_And_Call_Registered_ResourceTemplates()
146146
var resources = await client.ListResourceTemplatesAsync(TestContext.Current.CancellationToken);
147147
Assert.Equal(3, resources.Count);
148148

149-
var resource = resources.First(t => t.Name == nameof(SimpleResources.SomeNeatTemplatedResource));
149+
var resource = resources.First(t => t.Name == "some_neat_templated_resource");
150150
Assert.Equal("Some neat resource with parameters", resource.Description);
151151

152152
var result = await resource.ReadAsync(new Dictionary<string, object?>() { ["name"] = "hello" }, cancellationToken: TestContext.Current.CancellationToken);
@@ -204,13 +204,13 @@ public async Task TitleAttributeProperty_PropagatedToTitle()
204204
var resources = await client.ListResourcesAsync(cancellationToken: TestContext.Current.CancellationToken);
205205
Assert.NotNull(resources);
206206
Assert.NotEmpty(resources);
207-
McpClientResource resource = resources.First(t => t.Name == nameof(SimpleResources.SomeNeatDirectResource));
207+
McpClientResource resource = resources.First(t => t.Name == "some_neat_direct_resource");
208208
Assert.Equal("This is a title", resource.Title);
209209

210210
var resourceTemplates = await client.ListResourceTemplatesAsync(cancellationToken: TestContext.Current.CancellationToken);
211211
Assert.NotNull(resourceTemplates);
212212
Assert.NotEmpty(resourceTemplates);
213-
McpClientResourceTemplate resourceTemplate = resourceTemplates.First(t => t.Name == nameof(SimpleResources.SomeNeatTemplatedResource));
213+
McpClientResourceTemplate resourceTemplate = resourceTemplates.First(t => t.Name == "some_neat_templated_resource");
214214
Assert.Equal("This is another title", resourceTemplate.Title);
215215
}
216216

@@ -268,8 +268,8 @@ public void Register_Resources_From_Current_Assembly()
268268
sc.AddMcpServer().WithResourcesFromAssembly();
269269
IServiceProvider services = sc.BuildServiceProvider();
270270

271-
Assert.Contains(services.GetServices<McpServerResource>(), t => t.ProtocolResource?.Uri == $"resource://mcp/{nameof(SimpleResources.SomeNeatDirectResource)}");
272-
Assert.Contains(services.GetServices<McpServerResource>(), t => t.ProtocolResourceTemplate?.UriTemplate == $"resource://mcp/{nameof(SimpleResources.SomeNeatTemplatedResource)}{{?name}}");
271+
Assert.Contains(services.GetServices<McpServerResource>(), t => t.ProtocolResource?.Uri == $"resource://mcp/some_neat_direct_resource");
272+
Assert.Contains(services.GetServices<McpServerResource>(), t => t.ProtocolResourceTemplate?.UriTemplate == $"resource://mcp/some_neat_templated_resource{{?name}}");
273273
}
274274

275275
[Fact]
@@ -282,9 +282,9 @@ public void Register_Resources_From_Multiple_Sources()
282282
.WithResources([McpServerResource.Create(() => "42", new() { UriTemplate = "myResources:///returns42/{something}" })]);
283283
IServiceProvider services = sc.BuildServiceProvider();
284284

285-
Assert.Contains(services.GetServices<McpServerResource>(), t => t.ProtocolResource?.Uri == $"resource://mcp/{nameof(SimpleResources.SomeNeatDirectResource)}");
286-
Assert.Contains(services.GetServices<McpServerResource>(), t => t.ProtocolResourceTemplate?.UriTemplate == $"resource://mcp/{nameof(SimpleResources.SomeNeatTemplatedResource)}{{?name}}");
287-
Assert.Contains(services.GetServices<McpServerResource>(), t => t.ProtocolResourceTemplate?.UriTemplate == $"resource://mcp/{nameof(MoreResources.AnotherNeatDirectResource)}");
285+
Assert.Contains(services.GetServices<McpServerResource>(), t => t.ProtocolResource?.Uri == $"resource://mcp/some_neat_direct_resource");
286+
Assert.Contains(services.GetServices<McpServerResource>(), t => t.ProtocolResourceTemplate?.UriTemplate == $"resource://mcp/some_neat_templated_resource{{?name}}");
287+
Assert.Contains(services.GetServices<McpServerResource>(), t => t.ProtocolResourceTemplate?.UriTemplate == $"resource://mcp/another_neat_direct_resource");
288288
Assert.Contains(services.GetServices<McpServerResource>(), t => t.ProtocolResourceTemplate.UriTemplate == "myResources:///returns42/{something}");
289289
}
290290

0 commit comments

Comments
 (0)