From ce90d1b92a26ae58c3b2c630a9ff916171710fcf Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Fri, 27 Jun 2025 17:20:54 -0400 Subject: [PATCH 1/2] Change default name casing of McpServerXx.Create tools/prompts --- .../Server/AIFunctionMcpServerPrompt.cs | 2 +- .../Server/AIFunctionMcpServerTool.cs | 60 ++++++++++++++++++- .../MapMcpTests.cs | 2 +- .../SseIntegrationTests.cs | 4 +- .../McpServerBuilderExtensionsPromptsTests.cs | 16 ++--- .../McpServerBuilderExtensionsToolsTests.cs | 45 +++++++------- .../Configuration/McpServerScopedTests.cs | 2 +- .../Protocol/CancellationTests.cs | 2 +- 8 files changed, 94 insertions(+), 39 deletions(-) diff --git a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerPrompt.cs b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerPrompt.cs index ef463c374..432970324 100644 --- a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerPrompt.cs +++ b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerPrompt.cs @@ -68,7 +68,7 @@ private static AIFunctionFactoryOptions CreateAIFunctionFactoryOptions( MethodInfo method, McpServerPromptCreateOptions? options) => new() { - Name = options?.Name ?? method.GetCustomAttribute()?.Name, + Name = options?.Name ?? method.GetCustomAttribute()?.Name ?? AIFunctionMcpServerTool.DeriveName(method, JsonNamingPolicy.CamelCase), Description = options?.Description, MarshalResult = static (result, _, cancellationToken) => new ValueTask(result), SerializerOptions = options?.SerializerOptions ?? McpJsonUtilities.DefaultOptions, diff --git a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs index 596871f76..1674b24e3 100644 --- a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs +++ b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs @@ -8,6 +8,7 @@ using System.Reflection; using System.Text.Json; using System.Text.Json.Nodes; +using System.Text.RegularExpressions; namespace ModelContextProtocol.Server; @@ -74,7 +75,7 @@ private static AIFunctionFactoryOptions CreateAIFunctionFactoryOptions( MethodInfo method, McpServerToolCreateOptions? options) => new() { - Name = options?.Name ?? method.GetCustomAttribute()?.Name, + Name = options?.Name ?? method.GetCustomAttribute()?.Name ?? DeriveName(method, JsonNamingPolicy.SnakeCaseLower), Description = options?.Description, MarshalResult = static (result, _, cancellationToken) => new ValueTask(result), SerializerOptions = options?.SerializerOptions ?? McpJsonUtilities.DefaultOptions, @@ -293,6 +294,63 @@ public override async ValueTask InvokeAsync( }; } + /// Creates a name to use based on the supplied method and naming policy. + internal static string DeriveName(MethodInfo method, JsonNamingPolicy? policy) + { + 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?.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; + } + } + + /// Regex that flags runs of characters other than ASCII digits or letters. +#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; diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/MapMcpTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/MapMcpTests.cs index 7f94689b6..6635a8b93 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/MapMcpTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/MapMcpTests.cs @@ -80,7 +80,7 @@ IHttpContextAccessor is not currently supported with non-stateless Streamable HT await using var mcpClient = await ConnectAsync(); var response = await mcpClient.CallToolAsync( - "EchoWithUserName", + "echo_with_user_name", new Dictionary() { ["message"] = "Hello world!" }, cancellationToken: TestContext.Current.CancellationToken); diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/SseIntegrationTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/SseIntegrationTests.cs index 9d6810c52..756f9e4e6 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/SseIntegrationTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/SseIntegrationTests.cs @@ -149,11 +149,11 @@ public async Task AddMcpServer_CanBeCalled_MultipleTimes() 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 { ["message"] = "from client!" diff --git a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsPromptsTests.cs b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsPromptsTests.cs index 4a9a9a0cf..2a1ceedf2 100644 --- a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsPromptsTests.cs +++ b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsPromptsTests.cs @@ -100,7 +100,7 @@ public async Task Can_List_And_Call_Registered_Prompts() 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 == "returnsChatMessages"); Assert.Equal("Returns chat messages", prompt.Description); var result = await prompt.GetAsync(new Dictionary() { ["message"] = "hello" }, cancellationToken: TestContext.Current.CancellationToken); @@ -171,7 +171,7 @@ public async Task TitleAttributeProperty_PropagatedToTitle() Assert.NotNull(prompts); Assert.NotEmpty(prompts); - McpClientPrompt prompt = prompts.First(t => t.Name == nameof(SimplePrompts.ReturnsString)); + McpClientPrompt prompt = prompts.First(t => t.Name == "returnsString"); Assert.Equal("This is a title", prompt.Title); } @@ -204,7 +204,7 @@ public async Task Throws_Exception_Missing_Parameter() await using IMcpClient client = await CreateMcpClientForServer(); var e = await Assert.ThrowsAsync(async () => await client.GetPromptAsync( - nameof(SimplePrompts.ReturnsChatMessages), + "returnsChatMessages", cancellationToken: TestContext.Current.CancellationToken)); Assert.Equal(McpErrorCode.InternalError, e.ErrorCode); @@ -242,7 +242,7 @@ public void Register_Prompts_From_Current_Assembly() sc.AddMcpServer().WithPromptsFromAssembly(); IServiceProvider services = sc.BuildServiceProvider(); - Assert.Contains(services.GetServices(), t => t.ProtocolPrompt.Name == nameof(SimplePrompts.ReturnsChatMessages)); + Assert.Contains(services.GetServices(), t => t.ProtocolPrompt.Name == "returnsChatMessages"); } [Fact] @@ -255,10 +255,10 @@ public void Register_Prompts_From_Multiple_Sources() .WithPrompts([McpServerPrompt.Create(() => "42", new() { Name = "Returns42" })]); IServiceProvider services = sc.BuildServiceProvider(); - Assert.Contains(services.GetServices(), t => t.ProtocolPrompt.Name == nameof(SimplePrompts.ReturnsChatMessages)); - Assert.Contains(services.GetServices(), t => t.ProtocolPrompt.Name == nameof(SimplePrompts.ThrowsException)); - Assert.Contains(services.GetServices(), t => t.ProtocolPrompt.Name == nameof(SimplePrompts.ReturnsString)); - Assert.Contains(services.GetServices(), t => t.ProtocolPrompt.Name == nameof(MorePrompts.AnotherPrompt)); + Assert.Contains(services.GetServices(), t => t.ProtocolPrompt.Name == "returnsChatMessages"); + Assert.Contains(services.GetServices(), t => t.ProtocolPrompt.Name == "throwsException"); + Assert.Contains(services.GetServices(), t => t.ProtocolPrompt.Name == "returnsString"); + Assert.Contains(services.GetServices(), t => t.ProtocolPrompt.Name == "anotherPrompt"); Assert.Contains(services.GetServices(), t => t.ProtocolPrompt.Name == "Returns42"); } diff --git a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsToolsTests.cs b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsToolsTests.cs index ccacef663..38c688cce 100644 --- a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsToolsTests.cs +++ b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsToolsTests.cs @@ -126,8 +126,7 @@ public async Task Can_List_Registered_Tools() 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 async Task Can_Create_Multiple_Servers_From_Options_And_List_Registered_T 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 async Task Can_Call_Registered_Tool() await using IMcpClient client = await CreateMcpClientForServer(); var result = await client.CallToolAsync( - "Echo", + "echo", new Dictionary() { ["message"] = "Peter" }, cancellationToken: TestContext.Current.CancellationToken); @@ -250,7 +248,7 @@ public async Task Can_Call_Registered_Tool_With_Array_Result() await using IMcpClient client = await CreateMcpClientForServer(); var result = await client.CallToolAsync( - "EchoArray", + "echo_array", new Dictionary() { ["message"] = "Peter" }, cancellationToken: TestContext.Current.CancellationToken); @@ -274,7 +272,7 @@ public async Task Can_Call_Registered_Tool_With_Null_Result() 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 async Task Can_Call_Registered_Tool_With_Json_Result() 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 async Task Can_Call_Registered_Tool_With_Int_Result() 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 async Task Can_Call_Registered_Tool_And_Pass_ComplexType() await using IMcpClient client = await CreateMcpClientForServer(); var result = await client.CallToolAsync( - "EchoComplex", + "echo_complex", new Dictionary() { ["complex"] = JsonDocument.Parse("""{"Name": "Peter", "Age": 25}""").RootElement }, cancellationToken: TestContext.Current.CancellationToken); @@ -340,7 +338,7 @@ public async Task Can_Call_Registered_Tool_With_Instance_Method() 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 async Task Returns_IsError_Content_When_Tool_Fails() 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 async Task Returns_IsError_Missing_Parameter() 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 void Register_Tools_From_Current_Assembly() sc.AddMcpServer().WithToolsFromAssembly(); IServiceProvider services = sc.BuildServiceProvider(); - Assert.Contains(services.GetServices(), t => t.ProtocolTool.Name == "Echo"); + Assert.Contains(services.GetServices(), t => t.ProtocolTool.Name == "echo"); } [Theory] @@ -452,7 +450,7 @@ public void WithTools_Parameters_Satisfiable_From_DI(bool parameterInServices) sc.AddMcpServer().WithTools([typeof(EchoTool)], BuilderToolsJsonContext.Default.Options); IServiceProvider services = sc.BuildServiceProvider(); - McpServerTool tool = services.GetServices().First(t => t.ProtocolTool.Name == "EchoComplex"); + McpServerTool tool = services.GetServices().First(t => t.ProtocolTool.Name == "echo_complex"); if (parameterInServices) { Assert.DoesNotContain("\"complex\"", JsonSerializer.Serialize(tool.ProtocolTool.InputSchema, AIJsonUtilities.DefaultOptions)); @@ -495,7 +493,7 @@ public void WithToolsFromAssembly_Parameters_Satisfiable_From_DI(ServiceLifetime sc.AddMcpServer().WithToolsFromAssembly(); IServiceProvider services = sc.BuildServiceProvider(); - McpServerTool tool = services.GetServices().First(t => t.ProtocolTool.Name == "EchoComplex"); + McpServerTool tool = services.GetServices().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 async Task Recognizes_Parameter_Types() 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 void Register_Tools_From_Multiple_Sources() Assert.Contains(services.GetServices(), t => t.ProtocolTool.Name == "double_echo"); Assert.Contains(services.GetServices(), t => t.ProtocolTool.Name == "DifferentName"); - Assert.Contains(services.GetServices(), t => t.ProtocolTool.Name == "MethodB"); - Assert.Contains(services.GetServices(), t => t.ProtocolTool.Name == "MethodC"); - Assert.Contains(services.GetServices(), t => t.ProtocolTool.Name == "MethodD"); + Assert.Contains(services.GetServices(), t => t.ProtocolTool.Name == "method_b"); + Assert.Contains(services.GetServices(), t => t.ProtocolTool.Name == "method_c"); + Assert.Contains(services.GetServices(), t => t.ProtocolTool.Name == "method_d"); Assert.Contains(services.GetServices(), t => t.ProtocolTool.Name == "Returns42"); } @@ -591,7 +588,7 @@ public async Task TitleAttributeProperty_PropagatedToTitle() 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 async Task HandlesIProgressParameter() 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 async Task CancellationNotificationsPropagateToToolTokens() 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( diff --git a/tests/ModelContextProtocol.Tests/Configuration/McpServerScopedTests.cs b/tests/ModelContextProtocol.Tests/Configuration/McpServerScopedTests.cs index 8f90d2568..b940c1c7c 100644 --- a/tests/ModelContextProtocol.Tests/Configuration/McpServerScopedTests.cs +++ b/tests/ModelContextProtocol.Tests/Configuration/McpServerScopedTests.cs @@ -25,7 +25,7 @@ public async Task InjectScopedServiceAsArgument() 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; diff --git a/tests/ModelContextProtocol.Tests/Protocol/CancellationTests.cs b/tests/ModelContextProtocol.Tests/Protocol/CancellationTests.cs index 4def27938..80c6b1ed9 100644 --- a/tests/ModelContextProtocol.Tests/Protocol/CancellationTests.cs +++ b/tests/ModelContextProtocol.Tests/Protocol/CancellationTests.cs @@ -54,7 +54,7 @@ public async Task CancellationPropagation_RequestingCancellationCancelsPendingRe 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); From 499ff52db75855ece0f4303c431e5b9336a76912 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Tue, 1 Jul 2025 16:31:55 -0400 Subject: [PATCH 2/2] lower_snake_case all the things --- .../Server/AIFunctionMcpServerPrompt.cs | 2 +- .../Server/AIFunctionMcpServerResource.cs | 2 +- .../Server/AIFunctionMcpServerTool.cs | 6 +++--- .../McpServerBuilderExtensionsPromptsTests.cs | 16 ++++++++-------- ...McpServerBuilderExtensionsResourcesTests.cs | 18 +++++++++--------- 5 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerPrompt.cs b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerPrompt.cs index 432970324..d651d7ee3 100644 --- a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerPrompt.cs +++ b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerPrompt.cs @@ -68,7 +68,7 @@ private static AIFunctionFactoryOptions CreateAIFunctionFactoryOptions( MethodInfo method, McpServerPromptCreateOptions? options) => new() { - Name = options?.Name ?? method.GetCustomAttribute()?.Name ?? AIFunctionMcpServerTool.DeriveName(method, JsonNamingPolicy.CamelCase), + Name = options?.Name ?? method.GetCustomAttribute()?.Name ?? AIFunctionMcpServerTool.DeriveName(method), Description = options?.Description, MarshalResult = static (result, _, cancellationToken) => new ValueTask(result), SerializerOptions = options?.SerializerOptions ?? McpJsonUtilities.DefaultOptions, diff --git a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerResource.cs b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerResource.cs index 5412c4aca..a8b0d2486 100644 --- a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerResource.cs +++ b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerResource.cs @@ -75,7 +75,7 @@ private static AIFunctionFactoryOptions CreateAIFunctionFactoryOptions( MethodInfo method, McpServerResourceCreateOptions? options) => new() { - Name = options?.Name ?? method.GetCustomAttribute()?.Name, + Name = options?.Name ?? method.GetCustomAttribute()?.Name ?? AIFunctionMcpServerTool.DeriveName(method), Description = options?.Description, MarshalResult = static (result, _, cancellationToken) => new ValueTask(result), SerializerOptions = options?.SerializerOptions ?? McpJsonUtilities.DefaultOptions, diff --git a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs index 1674b24e3..afd3912b6 100644 --- a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs +++ b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs @@ -75,7 +75,7 @@ private static AIFunctionFactoryOptions CreateAIFunctionFactoryOptions( MethodInfo method, McpServerToolCreateOptions? options) => new() { - Name = options?.Name ?? method.GetCustomAttribute()?.Name ?? DeriveName(method, JsonNamingPolicy.SnakeCaseLower), + Name = options?.Name ?? method.GetCustomAttribute()?.Name ?? DeriveName(method), Description = options?.Description, MarshalResult = static (result, _, cancellationToken) => new ValueTask(result), SerializerOptions = options?.SerializerOptions ?? McpJsonUtilities.DefaultOptions, @@ -295,7 +295,7 @@ public override async ValueTask InvokeAsync( } /// Creates a name to use based on the supplied method and naming policy. - internal static string DeriveName(MethodInfo method, JsonNamingPolicy? policy) + internal static string DeriveName(MethodInfo method, JsonNamingPolicy? policy = null) { string name = method.Name; @@ -318,7 +318,7 @@ internal static string DeriveName(MethodInfo method, JsonNamingPolicy? policy) } // Case the name based on the provided naming policy. - return policy?.ConvertName(name) ?? name; + return (policy ?? JsonNamingPolicy.SnakeCaseLower).ConvertName(name) ?? name; static bool IsAsyncMethod(MethodInfo method) { diff --git a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsPromptsTests.cs b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsPromptsTests.cs index 2a1ceedf2..3fa2ec78b 100644 --- a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsPromptsTests.cs +++ b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsPromptsTests.cs @@ -100,7 +100,7 @@ public async Task Can_List_And_Call_Registered_Prompts() var prompts = await client.ListPromptsAsync(TestContext.Current.CancellationToken); Assert.Equal(6, prompts.Count); - var prompt = prompts.First(t => t.Name == "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() { ["message"] = "hello" }, cancellationToken: TestContext.Current.CancellationToken); @@ -171,7 +171,7 @@ public async Task TitleAttributeProperty_PropagatedToTitle() Assert.NotNull(prompts); Assert.NotEmpty(prompts); - McpClientPrompt prompt = prompts.First(t => t.Name == "returnsString"); + McpClientPrompt prompt = prompts.First(t => t.Name == "returns_string"); Assert.Equal("This is a title", prompt.Title); } @@ -204,7 +204,7 @@ public async Task Throws_Exception_Missing_Parameter() await using IMcpClient client = await CreateMcpClientForServer(); var e = await Assert.ThrowsAsync(async () => await client.GetPromptAsync( - "returnsChatMessages", + "returns_chat_messages", cancellationToken: TestContext.Current.CancellationToken)); Assert.Equal(McpErrorCode.InternalError, e.ErrorCode); @@ -242,7 +242,7 @@ public void Register_Prompts_From_Current_Assembly() sc.AddMcpServer().WithPromptsFromAssembly(); IServiceProvider services = sc.BuildServiceProvider(); - Assert.Contains(services.GetServices(), t => t.ProtocolPrompt.Name == "returnsChatMessages"); + Assert.Contains(services.GetServices(), t => t.ProtocolPrompt.Name == "returns_chat_messages"); } [Fact] @@ -255,10 +255,10 @@ public void Register_Prompts_From_Multiple_Sources() .WithPrompts([McpServerPrompt.Create(() => "42", new() { Name = "Returns42" })]); IServiceProvider services = sc.BuildServiceProvider(); - Assert.Contains(services.GetServices(), t => t.ProtocolPrompt.Name == "returnsChatMessages"); - Assert.Contains(services.GetServices(), t => t.ProtocolPrompt.Name == "throwsException"); - Assert.Contains(services.GetServices(), t => t.ProtocolPrompt.Name == "returnsString"); - Assert.Contains(services.GetServices(), t => t.ProtocolPrompt.Name == "anotherPrompt"); + Assert.Contains(services.GetServices(), t => t.ProtocolPrompt.Name == "returns_chat_messages"); + Assert.Contains(services.GetServices(), t => t.ProtocolPrompt.Name == "throws_exception"); + Assert.Contains(services.GetServices(), t => t.ProtocolPrompt.Name == "returns_string"); + Assert.Contains(services.GetServices(), t => t.ProtocolPrompt.Name == "another_prompt"); Assert.Contains(services.GetServices(), t => t.ProtocolPrompt.Name == "Returns42"); } diff --git a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsResourcesTests.cs b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsResourcesTests.cs index 3062136b4..ed930b174 100644 --- a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsResourcesTests.cs +++ b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsResourcesTests.cs @@ -129,7 +129,7 @@ public async Task Can_List_And_Call_Registered_Resources() 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 async Task Can_List_And_Call_Registered_ResourceTemplates() 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() { ["name"] = "hello" }, cancellationToken: TestContext.Current.CancellationToken); @@ -204,13 +204,13 @@ public async Task TitleAttributeProperty_PropagatedToTitle() 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 void Register_Resources_From_Current_Assembly() sc.AddMcpServer().WithResourcesFromAssembly(); IServiceProvider services = sc.BuildServiceProvider(); - Assert.Contains(services.GetServices(), t => t.ProtocolResource?.Uri == $"resource://mcp/{nameof(SimpleResources.SomeNeatDirectResource)}"); - Assert.Contains(services.GetServices(), t => t.ProtocolResourceTemplate?.UriTemplate == $"resource://mcp/{nameof(SimpleResources.SomeNeatTemplatedResource)}{{?name}}"); + Assert.Contains(services.GetServices(), t => t.ProtocolResource?.Uri == $"resource://mcp/some_neat_direct_resource"); + Assert.Contains(services.GetServices(), t => t.ProtocolResourceTemplate?.UriTemplate == $"resource://mcp/some_neat_templated_resource{{?name}}"); } [Fact] @@ -282,9 +282,9 @@ public void Register_Resources_From_Multiple_Sources() .WithResources([McpServerResource.Create(() => "42", new() { UriTemplate = "myResources:///returns42/{something}" })]); IServiceProvider services = sc.BuildServiceProvider(); - Assert.Contains(services.GetServices(), t => t.ProtocolResource?.Uri == $"resource://mcp/{nameof(SimpleResources.SomeNeatDirectResource)}"); - Assert.Contains(services.GetServices(), t => t.ProtocolResourceTemplate?.UriTemplate == $"resource://mcp/{nameof(SimpleResources.SomeNeatTemplatedResource)}{{?name}}"); - Assert.Contains(services.GetServices(), t => t.ProtocolResourceTemplate?.UriTemplate == $"resource://mcp/{nameof(MoreResources.AnotherNeatDirectResource)}"); + Assert.Contains(services.GetServices(), t => t.ProtocolResource?.Uri == $"resource://mcp/some_neat_direct_resource"); + Assert.Contains(services.GetServices(), t => t.ProtocolResourceTemplate?.UriTemplate == $"resource://mcp/some_neat_templated_resource{{?name}}"); + Assert.Contains(services.GetServices(), t => t.ProtocolResourceTemplate?.UriTemplate == $"resource://mcp/another_neat_direct_resource"); Assert.Contains(services.GetServices(), t => t.ProtocolResourceTemplate.UriTemplate == "myResources:///returns42/{something}"); }