diff --git a/Directory.Packages.props b/Directory.Packages.props index 136351699a..404131961d 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -69,7 +69,7 @@ - + @@ -87,8 +87,8 @@ - - + + diff --git a/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.Tests/Areas/Server/Commands/Runtime/McpRuntimeTests.cs b/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.Tests/Areas/Server/Commands/Runtime/McpRuntimeTests.cs index d65f0a42a4..272a19d587 100644 --- a/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.Tests/Areas/Server/Commands/Runtime/McpRuntimeTests.cs +++ b/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.Tests/Areas/Server/Commands/Runtime/McpRuntimeTests.cs @@ -38,21 +38,15 @@ private static IOptions CreateOptions(ServiceStartOptions? private static ITelemetryService CreateMockTelemetryService() => Substitute.For(); private static RequestContext CreateListToolsRequest() => - new(CreateMockServer(), new() { Method = RequestMethods.ToolsList }) - { - Params = new() - }; + new(CreateMockServer(), new() { Method = RequestMethods.ToolsList }, new()); private static RequestContext CreateCallToolRequest( string toolName = "test-tool", - IDictionary? arguments = null) => new(CreateMockServer(), new() { Method = RequestMethods.ToolsCall }) + IDictionary? arguments = null) => new(CreateMockServer(), new() { Method = RequestMethods.ToolsCall }, new() { - Params = new() - { - Name = toolName, - Arguments = arguments ?? new Dictionary() - } - }; + Name = toolName, + Arguments = arguments ?? new Dictionary() + }); private static object GetAndAssertTagKeyValue(Activity activity, string tagName) { @@ -171,7 +165,7 @@ public async Task ListToolsHandler_DelegatesToToolLoader() var mockTelemetry = CreateMockTelemetryService(); var activity = new Activity("test-activity"); - mockTelemetry.StartActivity(Arg.Any(), Arg.Any()) + mockTelemetry.StartActivity(Arg.Any(), Arg.Any(), Arg.Any()) .Returns(activity); var options = CreateOptions(); @@ -196,7 +190,7 @@ public async Task ListToolsHandler_DelegatesToToolLoader() Assert.Equal(expectedResult, result); await mockToolLoader.Received(1).ListToolsHandler(request, Arg.Any()); - mockTelemetry.Received(1).StartActivity(ActivityName.ListToolsHandler, Arg.Any()); + mockTelemetry.Received(1).StartActivity(ActivityName.ListToolsHandler, Arg.Any(), Arg.Any()); Assert.Equal(ActivityStatusCode.Ok, activity.Status); } @@ -210,7 +204,7 @@ public async Task CallToolHandler_DelegatesToToolLoader() var mockTelemetry = CreateMockTelemetryService(); var activity = new Activity("test-activity"); - mockTelemetry.StartActivity(Arg.Any(), Arg.Any()) + mockTelemetry.StartActivity(Arg.Any(), Arg.Any(), Arg.Any()) .Returns(activity); var options = CreateOptions(); @@ -240,7 +234,7 @@ public async Task CallToolHandler_DelegatesToToolLoader() Assert.Equal(expectedResult, result); await mockToolLoader.Received(1).CallToolHandler(request, Arg.Any()); - mockTelemetry.Received(1).StartActivity(ActivityName.ToolExecuted, Arg.Any()); + mockTelemetry.Received(1).StartActivity(ActivityName.ToolExecuted, Arg.Any(), Arg.Any()); Assert.Equal(ActivityStatusCode.Ok, activity.Status); var actualToolName = GetAndAssertTagKeyValue(activity, TagName.ToolName); @@ -316,7 +310,7 @@ public async Task ListToolsHandler_WhenToolLoaderThrows_PropagatesException() var mockTelemetry = CreateMockTelemetryService(); var activity = new Activity("test-activity"); - mockTelemetry.StartActivity(Arg.Any(), Arg.Any()) + mockTelemetry.StartActivity(Arg.Any(), Arg.Any(), Arg.Any()) .Returns(activity); var options = CreateOptions(); @@ -334,7 +328,7 @@ public async Task ListToolsHandler_WhenToolLoaderThrows_PropagatesException() Assert.Equal(expectedException.Message, actualException.Message); - mockTelemetry.Received(1).StartActivity(ActivityName.ListToolsHandler, Arg.Any()); + mockTelemetry.Received(1).StartActivity(ActivityName.ListToolsHandler, Arg.Any(), Arg.Any()); Assert.Equal(ActivityStatusCode.Error, activity.Status); GetAndAssertTagKeyValue(activity, TagName.ExceptionType); @@ -350,7 +344,7 @@ public async Task CallToolHandler_WhenToolLoaderThrows_PropagatesException() var mockTelemetry = CreateMockTelemetryService(); var activity = new Activity("test-activity"); - mockTelemetry.StartActivity(Arg.Any(), Arg.Any()) + mockTelemetry.StartActivity(Arg.Any(), Arg.Any(), Arg.Any()) .Returns(activity); var options = CreateOptions(); @@ -370,7 +364,7 @@ public async Task CallToolHandler_WhenToolLoaderThrows_PropagatesException() runtime.CallToolHandler(request, TestContext.Current.CancellationToken).AsTask()); Assert.Equal(expectedException.Message, actualException.Message); - mockTelemetry.Received(1).StartActivity(ActivityName.ToolExecuted, Arg.Any()); + mockTelemetry.Received(1).StartActivity(ActivityName.ToolExecuted, Arg.Any(), Arg.Any()); Assert.Equal(ActivityStatusCode.Error, activity.Status); var actualToolName = GetAndAssertTagKeyValue(activity, TagName.ToolName); @@ -450,7 +444,7 @@ public async Task ListToolsHandler_WithNullParameters_DelegatesToToolLoader() var mockToolLoader = Substitute.For(); var options = CreateOptions(); var runtime = new McpRuntime(mockToolLoader, options, CreateMockTelemetryService(), logger); - var request = new RequestContext(CreateMockServer(), new() { Method = RequestMethods.ToolsList }); + var request = new RequestContext(CreateMockServer(), new() { Method = RequestMethods.ToolsList }, null!); var expectedResult = new ListToolsResult { Tools = [] }; mockToolLoader.ListToolsHandler(request, Arg.Any()) @@ -475,11 +469,11 @@ public async Task CallToolHandler_WithNullParameters_ReturnsError() var mockTelemetry = CreateMockTelemetryService(); var activity = new Activity("test-activity"); - mockTelemetry.StartActivity(Arg.Any(), Arg.Any()) + mockTelemetry.StartActivity(Arg.Any(), Arg.Any(), Arg.Any()) .Returns(activity); var runtime = new McpRuntime(mockToolLoader, options, mockTelemetry, logger); - var request = new RequestContext(CreateMockServer(), new() { Method = RequestMethods.ToolsCall }); + var request = new RequestContext(CreateMockServer(), new() { Method = RequestMethods.ToolsCall }, null!); // Act var result = await runtime.CallToolHandler(request, TestContext.Current.CancellationToken); @@ -497,7 +491,7 @@ public async Task CallToolHandler_WithNullParameters_ReturnsError() // Verify that the tool loader was NOT called since the null request is handled at the runtime level await mockToolLoader.DidNotReceive().CallToolHandler(Arg.Any>(), Arg.Any()); - mockTelemetry.Received(1).StartActivity(ActivityName.ToolExecuted, Arg.Any()); + mockTelemetry.Received(1).StartActivity(ActivityName.ToolExecuted, Arg.Any(), Arg.Any()); Assert.Equal(ActivityStatusCode.Error, activity.Status); GetAndAssertTagKeyValue(activity, TagName.ExceptionType); } @@ -644,7 +638,7 @@ public async Task CallToolHandler_SetsActivityTags() var activity = new Activity("test"); var mockTelemetry = CreateMockTelemetryService(); - mockTelemetry.StartActivity(Arg.Any(), Arg.Any()).Returns(activity); + mockTelemetry.StartActivity(Arg.Any(), Arg.Any(), Arg.Any()).Returns(activity); var options = CreateOptions(); var runtime = new McpRuntime(mockToolLoader, options, mockTelemetry, logger); @@ -770,7 +764,7 @@ public async Task CallToolHandler_WithToolLoaderError_ShouldReturnErrorAndSetTel var mockTelemetry = CreateMockTelemetryService(); var activity = new Activity("test-activity"); - mockTelemetry.StartActivity(Arg.Any(), Arg.Any()) + mockTelemetry.StartActivity(Arg.Any(), Arg.Any(), Arg.Any()) .Returns(activity); var options = CreateOptions(); @@ -798,7 +792,7 @@ public async Task CallToolHandler_WithToolLoaderError_ShouldReturnErrorAndSetTel // Act var result = await runtime.CallToolHandler(request, TestContext.Current.CancellationToken); - mockTelemetry.Received(1).StartActivity(ActivityName.ToolExecuted, Arg.Any()); + mockTelemetry.Received(1).StartActivity(ActivityName.ToolExecuted, Arg.Any(), Arg.Any()); Assert.Equal(ActivityStatusCode.Error, activity.Status); GetAndAssertTagKeyValue(activity, TagName.ExceptionType); diff --git a/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.Tests/Areas/Server/Commands/ToolLoading/BaseToolLoaderTests.cs b/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.Tests/Areas/Server/Commands/ToolLoading/BaseToolLoaderTests.cs index 3d15bf19f1..f92a148a12 100644 --- a/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.Tests/Areas/Server/Commands/ToolLoading/BaseToolLoaderTests.cs +++ b/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.Tests/Areas/Server/Commands/ToolLoading/BaseToolLoaderTests.cs @@ -314,7 +314,7 @@ public async Task HandleSecretElicitation_WhenElicitationDisabled_ProceedsWithou Method = "tools/call", Params = JsonSerializer.SerializeToNode(new CallToolRequestParams { Name = "test-tool" }) }; - var request = new RequestContext(mockServer, jsonRpcRequest); + var request = new RequestContext(mockServer, jsonRpcRequest, null!); var logger = Substitute.For(); // Act @@ -342,7 +342,7 @@ public async Task HandleSecretElicitation_WhenClientDoesNotSupportElicitation_Re Method = "tools/call", Params = JsonSerializer.SerializeToNode(new CallToolRequestParams { Name = "test-tool" }) }; - var request = new RequestContext(mockServer, jsonRpcRequest); + var request = new RequestContext(mockServer, jsonRpcRequest, null!); var logger = Substitute.For(); // Act @@ -374,7 +374,7 @@ public async Task HandleSecretElicitation_WhenUserAccepts_ProceedsWithOperation( Method = "tools/call", Params = JsonSerializer.SerializeToNode(new CallToolRequestParams { Name = "test-tool" }) }; - var request = new RequestContext(mockServer, jsonRpcRequest); + var request = new RequestContext(mockServer, jsonRpcRequest, null!); var logger = Substitute.For(); // Act @@ -407,7 +407,7 @@ public async Task HandleSecretElicitation_WhenUserDeclines_RejectsOperation() Method = "tools/call", Params = JsonSerializer.SerializeToNode(new CallToolRequestParams { Name = "test-tool" }) }; - var request = new RequestContext(mockServer, jsonRpcRequest); + var request = new RequestContext(mockServer, jsonRpcRequest, null!); var logger = Substitute.For(); // Act @@ -446,7 +446,7 @@ public async Task HandleSecretElicitation_UsesDecisionEnumSchema() Method = "tools/call", Params = JsonSerializer.SerializeToNode(new CallToolRequestParams { Name = "test-tool" }) }; - var request = new RequestContext(mockServer, jsonRpcRequest); + var request = new RequestContext(mockServer, jsonRpcRequest, null!); var logger = Substitute.For(); // Act @@ -490,7 +490,7 @@ public async Task HandleSecretElicitation_WhenExceptionOccurs_ReturnsErrorResult Method = "tools/call", Params = JsonSerializer.SerializeToNode(new CallToolRequestParams { Name = "test-tool" }) }; - var request = new RequestContext(mockServer, jsonRpcRequest); + var request = new RequestContext(mockServer, jsonRpcRequest, null!); var logger = Substitute.For(); // Act @@ -503,17 +503,9 @@ public async Task HandleSecretElicitation_WhenExceptionOccurs_ReturnsErrorResult Assert.Contains("Elicitation failed", ((TextContentBlock)result.Content[0]).Text); } - internal sealed class TestableBaseToolLoader : BaseToolLoader + internal sealed class TestableBaseToolLoader(ILogger logger) : BaseToolLoader(logger) { - public TestableBaseToolLoader(ILogger logger) - : base(logger) - { - } - - public McpClientOptions CreateClientOptionsPublic(McpServer server) - { - return CreateClientOptions(server); - } + public McpClientOptions CreateClientOptionsPublic(McpServer server) => CreateClientOptions(server); public static Task HandleElicitationAsyncPublic( RequestContext request, diff --git a/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.Tests/Areas/Server/Commands/ToolLoading/CommandFactoryToolLoaderTests.cs b/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.Tests/Areas/Server/Commands/ToolLoading/CommandFactoryToolLoaderTests.cs index ea43ea506e..631a93cd88 100644 --- a/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.Tests/Areas/Server/Commands/ToolLoading/CommandFactoryToolLoaderTests.cs +++ b/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.Tests/Areas/Server/Commands/ToolLoading/CommandFactoryToolLoaderTests.cs @@ -11,6 +11,7 @@ using Microsoft.Mcp.Core.Helpers; using Microsoft.Mcp.Core.Models.Command; using ModelContextProtocol.Protocol; +using ModelContextProtocol.Server; using NSubstitute; using Xunit; @@ -30,13 +31,10 @@ private static (CommandFactoryToolLoader toolLoader, ICommandFactory commandFact return (toolLoader, commandFactory); } - private static ModelContextProtocol.Server.RequestContext CreateRequest() + private static RequestContext CreateRequest() { - var mockServer = Substitute.For(); - return new ModelContextProtocol.Server.RequestContext(mockServer, new() { Method = RequestMethods.ToolsList }) - { - Params = new ListToolsRequestParams() - }; + var mockServer = Substitute.For(); + return new RequestContext(mockServer, new() { Method = RequestMethods.ToolsList }, new()); } [Fact] @@ -323,15 +321,12 @@ public async Task CallToolHandler_WithValidTool_ExecutesSuccessfully() var availableCommands = CommandFactory.GetVisibleCommands(commandFactory.AllCommands); var firstCommand = availableCommands.First(); - var mockServer = Substitute.For(); - var request = new ModelContextProtocol.Server.RequestContext(mockServer, new() { Method = RequestMethods.ToolsCall }) + var mockServer = Substitute.For(); + var request = new RequestContext(mockServer, new() { Method = RequestMethods.ToolsCall }, new() { - Params = new CallToolRequestParams - { - Name = firstCommand.Key, - Arguments = new Dictionary() - } - }; + Name = firstCommand.Key, + Arguments = new Dictionary() + }); var result = await toolLoader.CallToolHandler(request, TestContext.Current.CancellationToken); @@ -345,11 +340,8 @@ public async Task CallToolHandler_WithNullParams_ReturnsError() { var (toolLoader, _) = CreateToolLoader(); - var mockServer = Substitute.For(); - var request = new ModelContextProtocol.Server.RequestContext(mockServer, new() { Method = RequestMethods.ToolsCall }) - { - Params = null - }; + var mockServer = Substitute.For(); + var request = new RequestContext(mockServer, new() { Method = RequestMethods.ToolsCall }, null!); var result = await toolLoader.CallToolHandler(request, TestContext.Current.CancellationToken); @@ -368,15 +360,12 @@ public async Task CallToolHandler_WithUnknownTool_ReturnsError() { var (toolLoader, _) = CreateToolLoader(); - var mockServer = Substitute.For(); - var request = new ModelContextProtocol.Server.RequestContext(mockServer, new() { Method = RequestMethods.ToolsCall }) + var mockServer = Substitute.For(); + var request = new RequestContext(mockServer, new() { Method = RequestMethods.ToolsCall }, new() { - Params = new CallToolRequestParams - { - Name = "non-existent-tool", - Arguments = new Dictionary() - } - }; + Name = "non-existent-tool", + Arguments = new Dictionary() + }); var result = await toolLoader.CallToolHandler(request, TestContext.Current.CancellationToken); @@ -459,17 +448,14 @@ public async Task CallToolHandler_BeforeListToolsHandler_ExecutesSuccessfully() var targetCommand = subscriptionListCommand; - var mockServer = Substitute.For(); + var mockServer = Substitute.For(); var arguments = new Dictionary(); - var callToolRequest = new ModelContextProtocol.Server.RequestContext(mockServer, new() { Method = RequestMethods.ToolsCall }) + var callToolRequest = new RequestContext(mockServer, new() { Method = RequestMethods.ToolsCall }, new() { - Params = new CallToolRequestParams - { - Name = targetCommand.Key, - Arguments = arguments - } - }; + Name = targetCommand.Key, + Arguments = arguments + }); // Act - Call CallToolHandler BEFORE ListToolsHandler var callResult = await toolLoader.CallToolHandler(callToolRequest, TestContext.Current.CancellationToken); @@ -608,17 +594,14 @@ public async Task CallToolHandler_WithSecretTool_WhenClientDoesNotSupportElicita commandMap["fake-secret-get"] = fakeCommand; // Create mock server without elicitation capabilities - var mockServer = Substitute.For(); + var mockServer = Substitute.For(); mockServer.ClientCapabilities.Returns((ClientCapabilities?)null); - var request = new ModelContextProtocol.Server.RequestContext(mockServer, new() { Method = RequestMethods.ToolsCall }) + var request = new RequestContext(mockServer, new() { Method = RequestMethods.ToolsCall }, new() { - Params = new CallToolRequestParams - { - Name = "fake-secret-get", - Arguments = new Dictionary() - } - }; + Name = "fake-secret-get", + Arguments = new Dictionary() + }); var result = await toolLoader.CallToolHandler(request, TestContext.Current.CancellationToken); @@ -648,18 +631,15 @@ public async Task CallToolHandler_WithNonSecretTool_DoesNotTriggerElicitation() commandMap["fake-non-secret-get"] = fakeCommand; // Create mock server with elicitation capabilities - var mockServer = Substitute.For(); + var mockServer = Substitute.For(); var capabilities = new ClientCapabilities { Elicitation = new ElicitationCapability() }; mockServer.ClientCapabilities.Returns(capabilities); - var request = new ModelContextProtocol.Server.RequestContext(mockServer, new() { Method = RequestMethods.ToolsCall }) + var request = new RequestContext(mockServer, new() { Method = RequestMethods.ToolsCall }, new() { - Params = new CallToolRequestParams - { - Name = "fake-non-secret-get", - Arguments = new Dictionary() - } - }; + Name = "fake-non-secret-get", + Arguments = new Dictionary() + }); var result = await toolLoader.CallToolHandler(request, TestContext.Current.CancellationToken); @@ -690,17 +670,14 @@ public async Task CallToolHandler_WithSecretTool_WhenDangerouslyDisableElicitati commandMap["fake-secret-get"] = fakeCommand; // Create mock server - elicitation support doesn't matter when bypassed - var mockServer = Substitute.For(); + var mockServer = Substitute.For(); mockServer.ClientCapabilities.Returns((ClientCapabilities?)null); - var request = new ModelContextProtocol.Server.RequestContext(mockServer, new() { Method = RequestMethods.ToolsCall }) + var request = new RequestContext(mockServer, new() { Method = RequestMethods.ToolsCall }, new() { - Params = new CallToolRequestParams - { - Name = "fake-secret-get", - Arguments = new Dictionary() - } - }; + Name = "fake-secret-get", + Arguments = new Dictionary() + }); var result = await toolLoader.CallToolHandler(request, TestContext.Current.CancellationToken); @@ -735,17 +712,14 @@ public async Task CallToolHandler_WithSecretTool_WhenDangerouslyDisableElicitati commandMap["fake-secret-get"] = fakeCommand; // Create mock server without elicitation capabilities - var mockServer = Substitute.For(); + var mockServer = Substitute.For(); mockServer.ClientCapabilities.Returns((ClientCapabilities?)null); - var request = new ModelContextProtocol.Server.RequestContext(mockServer, new() { Method = RequestMethods.ToolsCall }) + var request = new RequestContext(mockServer, new() { Method = RequestMethods.ToolsCall }, new() { - Params = new CallToolRequestParams - { - Name = "fake-secret-get", - Arguments = new Dictionary() - } - }; + Name = "fake-secret-get", + Arguments = new Dictionary() + }); var result = await toolLoader.CallToolHandler(request, TestContext.Current.CancellationToken); @@ -792,15 +766,12 @@ public async Task CallToolHandler_WithToolFilter_AllowsSpecifiedTool() var toolOptions = new ToolLoaderOptions { Tool = [specificToolName] }; var (toolLoader, _) = CreateToolLoader(toolOptions); - var mockServer = Substitute.For(); - var request = new ModelContextProtocol.Server.RequestContext(mockServer, new() { Method = RequestMethods.ToolsCall }) + var mockServer = Substitute.For(); + var request = new RequestContext(mockServer, new() { Method = RequestMethods.ToolsCall }, new() { - Params = new CallToolRequestParams - { - Name = specificToolName, - Arguments = new Dictionary() - } - }; + Name = specificToolName, + Arguments = new Dictionary() + }); // Act var result = await toolLoader.CallToolHandler(request, TestContext.Current.CancellationToken); @@ -835,15 +806,12 @@ public async Task CallToolHandler_WithToolFilter_RejectsNonSpecifiedTool() var toolOptions = new ToolLoaderOptions { Tool = [specificToolName] }; var (toolLoader, _) = CreateToolLoader(toolOptions); - var mockServer = Substitute.For(); - var request = new ModelContextProtocol.Server.RequestContext(mockServer, new() { Method = RequestMethods.ToolsCall }) + var mockServer = Substitute.For(); + var request = new RequestContext(mockServer, new() { Method = RequestMethods.ToolsCall }, new() { - Params = new CallToolRequestParams - { - Name = otherToolName, // Request a different tool than the filtered one - Arguments = new Dictionary() - } - }; + Name = otherToolName, // Request a different tool than the filtered one + Arguments = new Dictionary() + }); // Act var result = await toolLoader.CallToolHandler(request, TestContext.Current.CancellationToken); @@ -874,15 +842,12 @@ public async Task CallToolHandler_WithToolFilterCaseInsensitive_AllowsSpecifiedT var toolOptions = new ToolLoaderOptions { Tool = [specificToolName.ToUpperInvariant()] }; // Set filter to uppercase var (toolLoader, _) = CreateToolLoader(toolOptions); - var mockServer = Substitute.For(); - var request = new ModelContextProtocol.Server.RequestContext(mockServer, new() { Method = RequestMethods.ToolsCall }) + var mockServer = Substitute.For(); + var request = new RequestContext(mockServer, new() { Method = RequestMethods.ToolsCall }, new() { - Params = new CallToolRequestParams - { - Name = specificToolName, // Request with original case - Arguments = new Dictionary() - } - }; + Name = specificToolName, // Request with original case + Arguments = new Dictionary() + }); // Act var result = await toolLoader.CallToolHandler(request, TestContext.Current.CancellationToken); @@ -980,15 +945,12 @@ public async Task CallToolHandler_WithReadOnlyMode_RejectsNonReadOnlyTool() var commandMap = (Dictionary)commandMapField!.GetValue(commandFactory)!; commandMap["fake-write-tool"] = fakeCommand; - var mockServer = Substitute.For(); - var request = new ModelContextProtocol.Server.RequestContext(mockServer, new() { Method = RequestMethods.ToolsCall }) + var mockServer = Substitute.For(); + var request = new RequestContext(mockServer, new() { Method = RequestMethods.ToolsCall }, new() { - Params = new CallToolRequestParams - { - Name = "fake-write-tool", - Arguments = new Dictionary() - } - }; + Name = "fake-write-tool", + Arguments = new Dictionary() + }); // Act var result = await toolLoader.CallToolHandler(request, TestContext.Current.CancellationToken); @@ -1021,15 +983,12 @@ public async Task CallToolHandler_WithReadOnlyMode_AllowsReadOnlyTool() var commandMap = (Dictionary)commandMapField!.GetValue(commandFactory)!; commandMap["fake-readonly-tool"] = fakeCommand; - var mockServer = Substitute.For(); - var request = new ModelContextProtocol.Server.RequestContext(mockServer, new() { Method = RequestMethods.ToolsCall }) + var mockServer = Substitute.For(); + var request = new RequestContext(mockServer, new() { Method = RequestMethods.ToolsCall }, new() { - Params = new CallToolRequestParams - { - Name = "fake-readonly-tool", - Arguments = new Dictionary() - } - }; + Name = "fake-readonly-tool", + Arguments = new Dictionary() + }); // Act var result = await toolLoader.CallToolHandler(request, TestContext.Current.CancellationToken); @@ -1057,15 +1016,12 @@ public async Task CallToolHandler_WithHttpMode_RejectsLocalRequiredTool() var commandMap = (Dictionary)commandMapField!.GetValue(commandFactory)!; commandMap["fake-local-tool"] = fakeCommand; - var mockServer = Substitute.For(); - var request = new ModelContextProtocol.Server.RequestContext(mockServer, new() { Method = RequestMethods.ToolsCall }) + var mockServer = Substitute.For(); + var request = new RequestContext(mockServer, new() { Method = RequestMethods.ToolsCall }, new() { - Params = new CallToolRequestParams - { - Name = "fake-local-tool", - Arguments = new Dictionary() - } - }; + Name = "fake-local-tool", + Arguments = new Dictionary() + }); // Act var result = await toolLoader.CallToolHandler(request, TestContext.Current.CancellationToken); @@ -1098,15 +1054,12 @@ public async Task CallToolHandler_WithoutReadOnlyMode_AllowsNonReadOnlyTool() var commandMap = (Dictionary)commandMapField!.GetValue(commandFactory)!; commandMap["fake-write-tool-2"] = fakeCommand; - var mockServer = Substitute.For(); - var request = new ModelContextProtocol.Server.RequestContext(mockServer, new() { Method = RequestMethods.ToolsCall }) + var mockServer = Substitute.For(); + var request = new RequestContext(mockServer, new() { Method = RequestMethods.ToolsCall }, new() { - Params = new CallToolRequestParams - { - Name = "fake-write-tool-2", - Arguments = new Dictionary() - } - }; + Name = "fake-write-tool-2", + Arguments = new Dictionary() + }); // Act var result = await toolLoader.CallToolHandler(request, TestContext.Current.CancellationToken); diff --git a/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.Tests/Areas/Server/Commands/ToolLoading/CompositeToolLoaderTests.cs b/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.Tests/Areas/Server/Commands/ToolLoading/CompositeToolLoaderTests.cs index 11226bf436..82efeb2f81 100644 --- a/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.Tests/Areas/Server/Commands/ToolLoading/CompositeToolLoaderTests.cs +++ b/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.Tests/Areas/Server/Commands/ToolLoading/CompositeToolLoaderTests.cs @@ -22,23 +22,17 @@ private static IServiceProvider CreateServiceProvider() private static RequestContext CreateListToolsRequest() { var mockServer = Substitute.For(); - return new RequestContext(mockServer, new() { Method = RequestMethods.ToolsList }) - { - Params = new ListToolsRequestParams() - }; + return new RequestContext(mockServer, new() { Method = RequestMethods.ToolsList }, new()); } private static RequestContext CreateCallToolRequest(string toolName, IDictionary? arguments = null) { var mockServer = Substitute.For(); - return new RequestContext(mockServer, new() { Method = RequestMethods.ToolsCall }) + return new RequestContext(mockServer, new() { Method = RequestMethods.ToolsCall }, new() { - Params = new CallToolRequestParams - { - Name = toolName, - Arguments = arguments ?? new Dictionary() - } - }; + Name = toolName, + Arguments = arguments ?? new Dictionary() + }); } private static Tool CreateTestTool(string name, string description = "Test tool") @@ -286,10 +280,7 @@ public async Task CallToolHandler_WithNullParams_ReturnsErrorResult() var toolLoader = new CompositeToolLoader(toolLoaders, logger); var mockServer = Substitute.For(); - var request = new RequestContext(mockServer, new() { Method = RequestMethods.ToolsCall }) - { - Params = null - }; + var request = new RequestContext(mockServer, new() { Method = RequestMethods.ToolsCall }, null!); var result = await toolLoader.CallToolHandler(request, TestContext.Current.CancellationToken); diff --git a/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.Tests/Areas/Server/Commands/ToolLoading/NamespaceToolLoaderTests.cs b/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.Tests/Areas/Server/Commands/ToolLoading/NamespaceToolLoaderTests.cs index 7740464cb1..8ecd8d7b01 100644 --- a/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.Tests/Areas/Server/Commands/ToolLoading/NamespaceToolLoaderTests.cs +++ b/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.Tests/Areas/Server/Commands/ToolLoading/NamespaceToolLoaderTests.cs @@ -891,10 +891,7 @@ private string GetFirstAvailableNamespace() private static ModelContextProtocol.Server.RequestContext CreateListToolsRequest() { var mockServer = Substitute.For(); - return new(mockServer, new() { Method = RequestMethods.ToolsList }) - { - Params = new() - }; + return new(mockServer, new() { Method = RequestMethods.ToolsList }, new()); } private static ModelContextProtocol.Server.RequestContext CreateCallToolRequest( @@ -906,14 +903,11 @@ private static ModelContextProtocol.Server.RequestContext kvp => JsonSerializer.SerializeToElement(kvp.Value)); var mockServer = Substitute.For(); - return new(mockServer, new() { Method = RequestMethods.ToolsCall }) + return new(mockServer, new() { Method = RequestMethods.ToolsCall }, new() { - Params = new() - { - Name = toolName, - Arguments = jsonArguments - } - }; + Name = toolName, + Arguments = jsonArguments + }); } private static ModelContextProtocol.Server.RequestContext CreateCallToolRequestWithJsonElements( @@ -921,14 +915,11 @@ private static ModelContextProtocol.Server.RequestContext Dictionary arguments) { var mockServer = Substitute.For(); - return new(mockServer, new() { Method = RequestMethods.ToolsCall }) + return new(mockServer, new() { Method = RequestMethods.ToolsCall }, new() { - Params = new() - { - Name = toolName, - Arguments = arguments - } - }; + Name = toolName, + Arguments = arguments + }); } private static ModelContextProtocol.Client.McpClientOptions CallCreateClientOptions( diff --git a/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.Tests/Areas/Server/Commands/ToolLoading/RegistryToolLoaderTests.cs b/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.Tests/Areas/Server/Commands/ToolLoading/RegistryToolLoaderTests.cs index 49ae9e432b..6235a886ec 100644 --- a/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.Tests/Areas/Server/Commands/ToolLoading/RegistryToolLoaderTests.cs +++ b/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.Tests/Areas/Server/Commands/ToolLoading/RegistryToolLoaderTests.cs @@ -10,6 +10,7 @@ using Microsoft.Mcp.Core.Helpers; using ModelContextProtocol.Client; using ModelContextProtocol.Protocol; +using ModelContextProtocol.Server; using NSubstitute; using Xunit; @@ -29,26 +30,20 @@ private static (RegistryToolLoader toolLoader, IMcpDiscoveryStrategy mockDiscove return (toolLoader, mockDiscoveryStrategy); } - private static ModelContextProtocol.Server.RequestContext CreateListToolsRequest() + private static RequestContext CreateListToolsRequest() { - var mockServer = Substitute.For(); - return new ModelContextProtocol.Server.RequestContext(mockServer, new() { Method = RequestMethods.ToolsList }) - { - Params = new ListToolsRequestParams() - }; + var mockServer = Substitute.For(); + return new RequestContext(mockServer, new() { Method = RequestMethods.ToolsList }, new()); } - private static ModelContextProtocol.Server.RequestContext CreateCallToolRequest(string toolName, IDictionary? arguments = null) + private static RequestContext CreateCallToolRequest(string toolName, IDictionary? arguments = null) { - var mockServer = Substitute.For(); - return new ModelContextProtocol.Server.RequestContext(mockServer, new() { Method = RequestMethods.ToolsCall }) + var mockServer = Substitute.For(); + return new RequestContext(mockServer, new() { Method = RequestMethods.ToolsCall }, new() { - Params = new CallToolRequestParams - { - Name = toolName, - Arguments = arguments ?? new Dictionary() - } - }; + Name = toolName, + Arguments = arguments ?? new Dictionary() + }); } [Fact] diff --git a/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.Tests/Areas/Server/Commands/ToolLoading/ServerToolLoaderTests.cs b/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.Tests/Areas/Server/Commands/ToolLoading/ServerToolLoaderTests.cs index 54f9b38712..35c1bf1004 100644 --- a/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.Tests/Areas/Server/Commands/ToolLoading/ServerToolLoaderTests.cs +++ b/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.Tests/Areas/Server/Commands/ToolLoading/ServerToolLoaderTests.cs @@ -35,23 +35,17 @@ private static (ServerToolLoader toolLoader, IMcpDiscoveryStrategy mockDiscovery private static RequestContext CreateRequest() { var mockServer = Substitute.For(); - return new(mockServer, new() { Method = RequestMethods.ToolsList }) - { - Params = new() - }; + return new(mockServer, new() { Method = RequestMethods.ToolsList }, new()); } private static RequestContext CreateCallToolRequest(string toolName, IDictionary? arguments = null) { var mockServer = Substitute.For(); - return new(mockServer, new() { Method = RequestMethods.ToolsCall }) + return new(mockServer, new() { Method = RequestMethods.ToolsCall }, new() { - Params = new() - { - Name = toolName, - Arguments = arguments ?? new Dictionary() - } - }; + Name = toolName, + Arguments = arguments ?? new Dictionary() + }); } [Fact] @@ -275,14 +269,11 @@ private static RequestContext CreateCallToolRequestWithCo } var mockServer = Substitute.For(); - return new(mockServer, new() { Method = RequestMethods.ToolsCall }) + return new(mockServer, new() { Method = RequestMethods.ToolsCall }, new() { - Params = new() - { - Name = serverName, - Arguments = arguments - } - }; + Name = serverName, + Arguments = arguments + }); } [Fact] diff --git a/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.Tests/Areas/Server/Commands/ToolLoading/SingleProxyToolLoaderTests.cs b/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.Tests/Areas/Server/Commands/ToolLoading/SingleProxyToolLoaderTests.cs index 128c29a51a..e95fdbacd7 100644 --- a/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.Tests/Areas/Server/Commands/ToolLoading/SingleProxyToolLoaderTests.cs +++ b/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.Tests/Areas/Server/Commands/ToolLoading/SingleProxyToolLoaderTests.cs @@ -13,6 +13,7 @@ using Microsoft.Mcp.Core.Helpers; using ModelContextProtocol.Client; using ModelContextProtocol.Protocol; +using ModelContextProtocol.Server; using NSubstitute; using Xunit; @@ -74,28 +75,22 @@ private static (SingleProxyToolLoader toolLoader, IMcpDiscoveryStrategy discover } } - private static ModelContextProtocol.Server.RequestContext CreateListToolsRequest() + private static RequestContext CreateListToolsRequest() { - var mockServer = Substitute.For(); - return new ModelContextProtocol.Server.RequestContext(mockServer, new() { Method = RequestMethods.ToolsList }) - { - Params = new ListToolsRequestParams() - }; + var mockServer = Substitute.For(); + return new RequestContext(mockServer, new() { Method = RequestMethods.ToolsList }, new()); } - private static ModelContextProtocol.Server.RequestContext CreateCallToolRequest( + private static RequestContext CreateCallToolRequest( string toolName = "azure", Dictionary? arguments = null) { - var mockServer = Substitute.For(); - return new ModelContextProtocol.Server.RequestContext(mockServer, new() { Method = RequestMethods.ToolsCall }) + var mockServer = Substitute.For(); + return new RequestContext(mockServer, new() { Method = RequestMethods.ToolsCall }, new() { - Params = new CallToolRequestParams - { - Name = toolName, - Arguments = arguments ?? [] - } - }; + Name = toolName, + Arguments = arguments ?? [] + }); } [Fact] @@ -115,7 +110,7 @@ public async Task ListToolsHandler_ReturnsAzureToolWithExpectedSchema() var azureTool = result.Tools.FirstOrDefault(t => t.Name == "azure"); Assert.NotNull(azureTool); Assert.NotNull(azureTool.Description); - Assert.NotEmpty(azureTool.Description!); + Assert.NotEmpty(azureTool.Description); // Verify the tool has proper structure Assert.True(azureTool.InputSchema.ValueKind != JsonValueKind.Undefined); Assert.NotNull(azureTool.Annotations); @@ -248,11 +243,8 @@ public async Task CallToolHandler_WithNullParams_ReturnsGuidanceMessage() { // Arrange var (toolLoader, _) = CreateToolLoader(useRealDiscovery: true); - var mockServer = Substitute.For(); - var request = new ModelContextProtocol.Server.RequestContext(mockServer, new() { Method = RequestMethods.ToolsCall }) - { - Params = null - }; + var mockServer = Substitute.For(); + var request = new RequestContext(mockServer, new() { Method = RequestMethods.ToolsCall }, null!); // Act var result = await toolLoader.CallToolHandler(request, TestContext.Current.CancellationToken); @@ -427,7 +419,7 @@ private static SingleProxyToolLoader CreateToolLoaderWithMockClient( return new SingleProxyToolLoader(discoveryStrategy, logger, options, CreateServerConfigurationOptions()); } - private static ModelContextProtocol.Server.RequestContext CreateCallToolRequestWithToolAndCommand( + private static RequestContext CreateCallToolRequestWithToolAndCommand( string tool, string command) { var arguments = new Dictionary @@ -437,15 +429,12 @@ private static ModelContextProtocol.Server.RequestContext ["command"] = JsonDocument.Parse($"\"{command}\"").RootElement, }; - var mockServer = Substitute.For(); - return new(mockServer, new() { Method = RequestMethods.ToolsCall }) + var mockServer = Substitute.For(); + return new(mockServer, new() { Method = RequestMethods.ToolsCall }, new() { - Params = new CallToolRequestParams - { - Name = "azure", - Arguments = arguments - } - }; + Name = "azure", + Arguments = arguments + }); } [Fact] diff --git a/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.Tests/Areas/Server/ServerStartCommandTests.cs b/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.Tests/Areas/Server/ServerStartCommandTests.cs index c9de6ee7b5..b72dc809cc 100644 --- a/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.Tests/Areas/Server/ServerStartCommandTests.cs +++ b/core/Azure.Mcp.Core/tests/Azure.Mcp.Core.Tests/Areas/Server/ServerStartCommandTests.cs @@ -537,7 +537,7 @@ public async Task AllMode_WithReadOnlyFlag_LoadsOnlyReadOnlyTools() public async Task InvalidMode_FailsToStartServer() { // Act & Assert - await Assert.ThrowsAsync(async () => + await Assert.ThrowsAsync(async () => { await using var client = await CreateClientAsync("server", "start", "--mode", "invalid-mode"); }); diff --git a/core/Microsoft.Mcp.Core/src/Areas/Server/Commands/Runtime/McpRuntime.cs b/core/Microsoft.Mcp.Core/src/Areas/Server/Commands/Runtime/McpRuntime.cs index 506a4b051c..21de43719d 100644 --- a/core/Microsoft.Mcp.Core/src/Areas/Server/Commands/Runtime/McpRuntime.cs +++ b/core/Microsoft.Mcp.Core/src/Areas/Server/Commands/Runtime/McpRuntime.cs @@ -59,7 +59,7 @@ public McpRuntime( /// A result containing the output of the tool invocation. public async ValueTask CallToolHandler(RequestContext request, CancellationToken cancellationToken) { - using var activity = _telemetry.StartActivity(ActivityName.ToolExecuted, request.Server.ClientInfo); + using var activity = _telemetry.StartActivity(ActivityName.ToolExecuted, request.Server.ClientInfo, request.Params); CaptureToolCallMeta(activity, request.Params?.Meta); if (request.Params == null) @@ -169,7 +169,7 @@ private static void CaptureToolCallMeta(Activity? activity, JsonObject? meta) /// A result containing the list of available tools. public async ValueTask ListToolsHandler(RequestContext request, CancellationToken cancellationToken) { - using var activity = _telemetry.StartActivity(ActivityName.ListToolsHandler, request.Server.ClientInfo); + using var activity = _telemetry.StartActivity(ActivityName.ListToolsHandler, request.Server.ClientInfo, request.Params); try { diff --git a/core/Microsoft.Mcp.Core/src/Areas/Server/Commands/ToolLoading/CompositeToolLoader.cs b/core/Microsoft.Mcp.Core/src/Areas/Server/Commands/ToolLoading/CompositeToolLoader.cs index 4c1de671af..5c324a18d3 100644 --- a/core/Microsoft.Mcp.Core/src/Areas/Server/Commands/ToolLoading/CompositeToolLoader.cs +++ b/core/Microsoft.Mcp.Core/src/Areas/Server/Commands/ToolLoading/CompositeToolLoader.cs @@ -165,10 +165,7 @@ private async Task InitializeAsync(McpServer server, CancellationToken cancellat var allTools = new List(); // Create a request for listing tools to populate the tool loader map - var listToolsRequest = new RequestContext(server, new() { Method = RequestMethods.ToolsList }) - { - Params = new ListToolsRequestParams() - }; + var listToolsRequest = new RequestContext(server, new() { Method = RequestMethods.ToolsList }, new()); foreach (var loader in _toolLoaders) { diff --git a/core/Microsoft.Mcp.Core/src/Helpers/McpHelper.cs b/core/Microsoft.Mcp.Core/src/Helpers/McpHelper.cs index 287a0a755b..6c2fa741ff 100644 --- a/core/Microsoft.Mcp.Core/src/Helpers/McpHelper.cs +++ b/core/Microsoft.Mcp.Core/src/Helpers/McpHelper.cs @@ -16,6 +16,13 @@ public static class McpHelper public const string LocalRequiredHintMetaKey = "LocalRequiredHint"; public const string ToolIdMetaKey = "MicrosoftMcpToolId"; + /// + /// Key used in _meta for stateless handling beginning in 2026-07-28 spec. + /// + public const string ClientInfoMetaKey = "io.modelcontextprotocol/clientInfo"; + public const string ClientInfoNameKey = "name"; + public const string ClientInfoVersionKey = "version"; + /// /// Determines whether the tool has the hint in its metadata and is true. /// diff --git a/core/Microsoft.Mcp.Core/src/Services/Telemetry/ITelemetryService.cs b/core/Microsoft.Mcp.Core/src/Services/Telemetry/ITelemetryService.cs index babc8f8924..792faea522 100644 --- a/core/Microsoft.Mcp.Core/src/Services/Telemetry/ITelemetryService.cs +++ b/core/Microsoft.Mcp.Core/src/Services/Telemetry/ITelemetryService.cs @@ -21,9 +21,10 @@ public interface ITelemetryService : IDisposable /// /// Name of the activity. /// The MCP client information to add to the activity. + /// The request parameters for the MCP call. Starting in MCP 2026-07-28 spec, this contains MCP client info.An Activity object or null if there are no active listeners or telemetry is disabled. /// If the service is not in an operational state or was not invoked. - Activity? StartActivity(string activityName, Implementation? clientInfo); + Activity? StartActivity(string activityName, Implementation? clientInfo, RequestParams? requestParams); /// /// Performs any initialization operations before telemetry service is ready. diff --git a/core/Microsoft.Mcp.Core/src/Services/Telemetry/NoopTelemetryService.cs b/core/Microsoft.Mcp.Core/src/Services/Telemetry/NoopTelemetryService.cs index f3969cf446..223425cf4d 100644 --- a/core/Microsoft.Mcp.Core/src/Services/Telemetry/NoopTelemetryService.cs +++ b/core/Microsoft.Mcp.Core/src/Services/Telemetry/NoopTelemetryService.cs @@ -21,5 +21,5 @@ public void Dispose() public Activity? StartActivity(string activityName) => null; - public Activity? StartActivity(string activityName, Implementation? clientInfo) => null; + public Activity? StartActivity(string activityName, Implementation? clientInfo, RequestParams? requestParams) => null; } diff --git a/core/Microsoft.Mcp.Core/src/Services/Telemetry/TelemetryService.cs b/core/Microsoft.Mcp.Core/src/Services/Telemetry/TelemetryService.cs index 8663852a96..599ba371f8 100644 --- a/core/Microsoft.Mcp.Core/src/Services/Telemetry/TelemetryService.cs +++ b/core/Microsoft.Mcp.Core/src/Services/Telemetry/TelemetryService.cs @@ -3,11 +3,13 @@ using System.Diagnostics; using System.Runtime.InteropServices; +using System.Text.Json.Nodes; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Mcp.Core.Areas.Server.Options; using Microsoft.Mcp.Core.Commands; using Microsoft.Mcp.Core.Configuration; +using Microsoft.Mcp.Core.Helpers; using Microsoft.Mcp.Core.Services.Azure.Authentication; using ModelContextProtocol.Protocol; @@ -80,12 +82,12 @@ public TelemetryService(IMachineInformationProvider informationProvider, /// /// /// - public Activity? StartActivity(string activityName) => StartActivity(activityName, null); + public Activity? StartActivity(string activityName) => StartActivity(activityName, null, null); /// /// /// - public Activity? StartActivity(string activityName, Implementation? clientInfo) + public Activity? StartActivity(string activityName, Implementation? clientInfo, RequestParams? requestParams) { if (!_isEnabled) { @@ -101,11 +103,7 @@ public TelemetryService(IMachineInformationProvider informationProvider, return activity; } - if (clientInfo != null) - { - activity.AddTag(TagName.ClientName, clientInfo.Name) - .AddTag(TagName.ClientVersion, clientInfo.Version); - } + SetClientNameAndVersion(activity, clientInfo, requestParams); activity.AddTag(TagName.EventId, Guid.NewGuid().ToString()); @@ -114,6 +112,33 @@ public TelemetryService(IMachineInformationProvider informationProvider, return activity; } + internal static void SetClientNameAndVersion(Activity activity, Implementation? clientInfo, RequestParams? requestParams) + { + if (clientInfo != null) + { + activity.SetTag(TagName.ClientName, clientInfo.Name) + .SetTag(TagName.ClientVersion, clientInfo.Version); + } + + if (requestParams?.Meta != null && + requestParams.Meta.TryGetPropertyValue(McpHelper.ClientInfoMetaKey, out var node) && + node is JsonObject requestClientInfo) + { + if (requestClientInfo.TryGetPropertyValue(McpHelper.ClientInfoNameKey, out var nameNode) && + nameNode is JsonValue nameValue && + nameValue.TryGetValue(out var nameString)) + { + activity.SetTag(TagName.ClientName, nameString); + } + if (requestClientInfo.TryGetPropertyValue(McpHelper.ClientInfoVersionKey, out var versionNode) && + versionNode is JsonValue versionValue && + versionValue.TryGetValue(out var versionString)) + { + activity.SetTag(TagName.ClientVersion, versionString); + } + } + } + public void Dispose() { } @@ -139,10 +164,7 @@ public async Task InitializeAsync() { // Check after acquiring lock to ensure we honor work // started while we were waiting. - if (_initalizationTask == null) - { - _initalizationTask = InnerInitializeAsync(); - } + _initalizationTask ??= InnerInitializeAsync(); } finally { diff --git a/core/Microsoft.Mcp.Core/tests/Microsoft.Mcp.Core.Tests/Areas/Server/Commands/ToolLoading/ToolLoaderTelemetryTests.cs b/core/Microsoft.Mcp.Core/tests/Microsoft.Mcp.Core.Tests/Areas/Server/Commands/ToolLoading/ToolLoaderTelemetryTests.cs index 81f117fe9d..4d9b57ab29 100644 --- a/core/Microsoft.Mcp.Core/tests/Microsoft.Mcp.Core.Tests/Areas/Server/Commands/ToolLoading/ToolLoaderTelemetryTests.cs +++ b/core/Microsoft.Mcp.Core/tests/Microsoft.Mcp.Core.Tests/Areas/Server/Commands/ToolLoading/ToolLoaderTelemetryTests.cs @@ -193,7 +193,7 @@ private IMcpRuntime CreateRuntime(IToolLoader toolLoader) { var options = Microsoft.Extensions.Options.Options.Create(new ServiceStartOptions()); var telemetry = Substitute.For(); - telemetry.StartActivity(Arg.Any(), Arg.Any()).Returns(_activity); + telemetry.StartActivity(Arg.Any(), Arg.Any(), Arg.Any()).Returns(_activity); var logger = Substitute.For>(); var runtime = new McpRuntime(toolLoader, options, telemetry, logger); @@ -203,14 +203,11 @@ private IMcpRuntime CreateRuntime(IToolLoader toolLoader) private static RequestContext CreateToolCallRequest(McpServer mcpServer, string toolName) { - return new RequestContext(mcpServer, CreateJsonRpcRequest("tools/call")) + return new RequestContext(mcpServer, CreateJsonRpcRequest("tools/call"), new() { - Params = new CallToolRequestParams() - { - Name = toolName, - Arguments = new Dictionary() - } - }; + Name = toolName, + Arguments = new Dictionary() + }); } private static JsonRpcRequest CreateJsonRpcRequest(string method) diff --git a/core/Microsoft.Mcp.Core/tests/Microsoft.Mcp.Core.Tests/Services/Telemetry/TelemetryServiceTests.cs b/core/Microsoft.Mcp.Core/tests/Microsoft.Mcp.Core.Tests/Services/Telemetry/TelemetryServiceTests.cs index 54ea174e03..3d830f006f 100644 --- a/core/Microsoft.Mcp.Core/tests/Microsoft.Mcp.Core.Tests/Services/Telemetry/TelemetryServiceTests.cs +++ b/core/Microsoft.Mcp.Core/tests/Microsoft.Mcp.Core.Tests/Services/Telemetry/TelemetryServiceTests.cs @@ -1,13 +1,16 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Diagnostics; using System.Runtime.InteropServices; +using System.Text.Json.Nodes; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Mcp.Core.Areas.Server.Options; using Microsoft.Mcp.Core.Commands; using Microsoft.Mcp.Core.Configuration; +using Microsoft.Mcp.Core.Helpers; using Microsoft.Mcp.Core.Services.Azure.Authentication; using Microsoft.Mcp.Core.Services.Telemetry; using ModelContextProtocol.Protocol; @@ -83,7 +86,7 @@ public void StartActivity_WithClientInfo_WhenTelemetryDisabled_ShouldReturnNull( }; // Act - using var activity = service.StartActivity(activityId, clientInfo); + using var activity = service.StartActivity(activityId, clientInfo, null); // Assert Assert.Null(activity); @@ -218,7 +221,7 @@ public void StartActivity_WithoutInitialization_Throws() Version = "1.0.0", Title = "Test MCP server" }; - Assert.Throws(() => service.StartActivity("an-activity-id", clientInfo)); + Assert.Throws(() => service.StartActivity("an-activity-id", clientInfo, null)); } [Fact] @@ -253,7 +256,7 @@ public async Task StartActivity_WhenInitializationFails_Throws() await Assert.ThrowsAsync(() => service.InitializeAsync()); - Assert.Throws(() => service.StartActivity("an-activity-id", clientInfo)); + Assert.Throws(() => service.StartActivity("an-activity-id", clientInfo, null)); } [Fact] @@ -545,4 +548,168 @@ private class ExceptionalInformationProvider : IMachineInformationProvider public Task GetOrCreateDeviceId() => Task.FromException( new ArgumentNullException("test-exception")); } + + [Fact] + public void SetClientNameAndVersion_NoImplementationOrRequestParams_NothingIsSet() + { + var activity = new Activity("test"); + TelemetryService.SetClientNameAndVersion(activity, null, null); + + ValidateClientNameAndVersion(activity, null, null); + } + + [Fact] + public void SetClientNameAndVersion_ImplementationOnly_SetsCorrectly() + { + var activity = new Activity("test"); + var clientInfo = new Implementation + { + Name = "Foo-Bar-MCP", + Version = "1.0.0", + }; + TelemetryService.SetClientNameAndVersion(activity, clientInfo, null); + + ValidateClientNameAndVersion(activity, clientInfo.Name, clientInfo.Version); + } + + [Fact] + public void SetClientNameAndVersion_RequestParamsOnly_SetsCorrectly() + { + var activity = new Activity("test"); + var requestParams = new ListToolsRequestParams() + { + Meta = CreateClientInfo("Fizz-Buzz-MCP", "2.0.0") + }; + TelemetryService.SetClientNameAndVersion(activity, null, requestParams); + + ValidateClientNameAndVersion(activity, "Fizz-Buzz-MCP", "2.0.0"); + } + + [Fact] + public void SetClientNameAndVersion_BothImplementationAndRequestParams_SetsCorrectly() + { + var activity = new Activity("test"); + var clientInfo = new Implementation + { + Name = "Foo-Bar-MCP", + Version = "1.0.0", + }; + var requestParams = new ListToolsRequestParams() + { + Meta = CreateClientInfo("Fizz-Buzz-MCP", "2.0.0") + }; + TelemetryService.SetClientNameAndVersion(activity, clientInfo, requestParams); + + ValidateClientNameAndVersion(activity, "Fizz-Buzz-MCP", "2.0.0"); + } + + [Theory] + [InlineData(null, null)] + [InlineData("Fizz-Buzz-MCP", null)] + [InlineData(null, "2.0.0")] + public void SetClientNameAndVersion_RequestParamsPartial_HandlesCorrectly(string? name, string? version) + { + var activity = new Activity("test"); + var requestParams = new ListToolsRequestParams() + { + Meta = CreateClientInfo(name, version) + }; + TelemetryService.SetClientNameAndVersion(activity, null, requestParams); + + ValidateClientNameAndVersion(activity, name, version); + } + + [Theory] + [InlineData(true, false)] + [InlineData(1, 2)] + [InlineData(1.0, 2.0)] + public void SetClientNameAndVersion_RequestParamsNonString_AreIgnored(object name, object version) + { + var activity = new Activity("test"); + var requestParams = new ListToolsRequestParams() + { + Meta = CreateClientInfo(name, version) + }; + TelemetryService.SetClientNameAndVersion(activity, null, requestParams); + + ValidateClientNameAndVersion(activity, null, null); + } + + [Fact] + public void SetClientNameAndVersion_RequestParamsMissingClientInfo_HandlesCorrectly() + { + var activity = new Activity("test"); + var requestParams = new ListToolsRequestParams() + { + Meta = new([]) + }; + TelemetryService.SetClientNameAndVersion(activity, null, requestParams); + + ValidateClientNameAndVersion(activity, null, null); + } + + [Fact] + public void SetClientNameAndVersion_RequestParamsArrayClientInfo_HandlesCorrectly() + { + var activity = new Activity("test"); + var requestParams = new ListToolsRequestParams() + { + Meta = new([new(McpHelper.ClientInfoMetaKey, new JsonArray())]) + }; + TelemetryService.SetClientNameAndVersion(activity, null, requestParams); + + ValidateClientNameAndVersion(activity, null, null); + } + + [Fact] + public void SetClientNameAndVersion_RequestParamsValueClientInfo_HandlesCorrectly() + { + var activity = new Activity("test"); + var requestParams = new ListToolsRequestParams() + { + Meta = new([new(McpHelper.ClientInfoMetaKey, "string")]) + }; + TelemetryService.SetClientNameAndVersion(activity, null, requestParams); + + ValidateClientNameAndVersion(activity, null, null); + } + + private static JsonObject CreateClientInfo(object? name, object? version) + { + var jsonObject = new JsonObject(); + if (name != null) + { + jsonObject[McpHelper.ClientInfoNameKey] = JsonValue.Create(name); + } + if (version != null) + { + jsonObject[McpHelper.ClientInfoVersionKey] = JsonValue.Create(version); + } + + return new([new(McpHelper.ClientInfoMetaKey, jsonObject)]); + } + + private static void ValidateClientNameAndVersion(Activity activity, string? name, string? version) + { + var dictionary = activity.Tags.ToDictionary(); + if (name == null) + { + Assert.False(dictionary.ContainsKey(TagName.ClientName)); + } + else + { + var actualName = Assert.Contains(TagName.ClientName, dictionary); + Assert.Equal(name, actualName); + } + + if (version == null) + { + Assert.False(dictionary.ContainsKey(TagName.ClientVersion)); + } + else + { + var actualVersion = Assert.Contains(TagName.ClientVersion, dictionary); + Assert.Equal(version, actualVersion); + } + } } diff --git a/eng/tools/ToolMetadataExporter/src/Program.cs b/eng/tools/ToolMetadataExporter/src/Program.cs index 83272812f2..9c57ac4c15 100644 --- a/eng/tools/ToolMetadataExporter/src/Program.cs +++ b/eng/tools/ToolMetadataExporter/src/Program.cs @@ -84,7 +84,7 @@ private static void ConfigureAzureServices(IServiceCollection services) services.AddScoped(sp => { var credential = new ChainedTokenCredential( - new ManagedIdentityCredential(), + new ManagedIdentityCredential(new ManagedIdentityCredentialOptions()), new DefaultAzureCredential() );