From 8e160938114cfd9f4063a7b366c1f04a5abcee86 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 10 Feb 2026 00:42:56 +0000
Subject: [PATCH 1/6] Initial plan
From ee969d270f0a79c7c3d54512ff278777e0c5bf80 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 10 Feb 2026 00:50:08 +0000
Subject: [PATCH 2/6] Add ToolCallCompleted success log with tool name in
McpServerImpl
Add a LoggerMessage-based ToolCallCompleted log at Information level that
captures the tool name on successful tool call completion, matching the
existing ToolCallError pattern for failures.
Also add the log call to the task-augmented tool execution path.
Add test Logs_Tool_Name_On_Successful_Call to verify the new log message.
Co-authored-by: halter73 <54385+halter73@users.noreply.github.com>
---
.../Server/McpServerImpl.cs | 8 +++++++-
.../McpServerBuilderExtensionsToolsTests.cs | 17 +++++++++++++++++
2 files changed, 24 insertions(+), 1 deletion(-)
diff --git a/src/ModelContextProtocol.Core/Server/McpServerImpl.cs b/src/ModelContextProtocol.Core/Server/McpServerImpl.cs
index e58857dab..20286ce42 100644
--- a/src/ModelContextProtocol.Core/Server/McpServerImpl.cs
+++ b/src/ModelContextProtocol.Core/Server/McpServerImpl.cs
@@ -610,7 +610,9 @@ await originalListToolsHandler(request, cancellationToken).ConfigureAwait(false)
try
{
- return await handler(request, cancellationToken).ConfigureAwait(false);
+ var result = await handler(request, cancellationToken).ConfigureAwait(false);
+ ToolCallCompleted(request.Params?.Name ?? string.Empty);
+ return result;
}
catch (Exception e) when (e is not OperationCanceledException and not McpProtocolException)
{
@@ -944,6 +946,9 @@ internal static LoggingLevel ToLoggingLevel(LogLevel level) =>
[LoggerMessage(Level = LogLevel.Error, Message = "\"{ToolName}\" threw an unhandled exception.")]
private partial void ToolCallError(string toolName, Exception exception);
+ [LoggerMessage(Level = LogLevel.Information, Message = "\"{ToolName}\" completed successfully.")]
+ private partial void ToolCallCompleted(string toolName);
+
///
/// Executes a tool call as a task and returns a CallToolTaskResult immediately.
///
@@ -1004,6 +1009,7 @@ private async ValueTask ExecuteToolAsTaskAsync(
// Invoke the tool with task-specific cancellation token
var result = await tool.InvokeAsync(request, taskCancellationToken).ConfigureAwait(false);
+ ToolCallCompleted(request.Params?.Name ?? string.Empty);
// Determine final status based on whether there was an error
var finalStatus = result.IsError is true ? McpTaskStatus.Failed : McpTaskStatus.Completed;
diff --git a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsToolsTests.cs b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsToolsTests.cs
index a0fb3cbe4..f3972e6fb 100644
--- a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsToolsTests.cs
+++ b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsToolsTests.cs
@@ -380,6 +380,23 @@ public async Task Returns_IsError_Content_And_Logs_Error_When_Tool_Fails()
Assert.Equal("Test error", errorLog.Exception.Message);
}
+ [Fact]
+ public async Task Logs_Tool_Name_On_Successful_Call()
+ {
+ await using McpClient client = await CreateMcpClientForServer();
+
+ var result = await client.CallToolAsync(
+ "echo",
+ new Dictionary { ["message"] = "test" },
+ cancellationToken: TestContext.Current.CancellationToken);
+
+ Assert.True(result.IsError is not true);
+ Assert.Equal("hello test", (result.Content[0] as TextContentBlock)?.Text);
+
+ var infoLog = Assert.Single(MockLoggerProvider.LogMessages, m => m.Message == "\"echo\" completed successfully.");
+ Assert.Equal(LogLevel.Information, infoLog.LogLevel);
+ }
+
[Fact]
public async Task Throws_Exception_On_Unknown_Tool()
{
From 88dc7713bba4dbb43ef99e102ece55e630967338 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 10 Feb 2026 00:52:43 +0000
Subject: [PATCH 3/6] Move ToolCallCompleted log after IsError check in task
path
Address code review: only log ToolCallCompleted when the task-augmented
tool execution result indicates success (not IsError).
Co-authored-by: halter73 <54385+halter73@users.noreply.github.com>
---
src/ModelContextProtocol.Core/Server/McpServerImpl.cs | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/src/ModelContextProtocol.Core/Server/McpServerImpl.cs b/src/ModelContextProtocol.Core/Server/McpServerImpl.cs
index 20286ce42..ace30500a 100644
--- a/src/ModelContextProtocol.Core/Server/McpServerImpl.cs
+++ b/src/ModelContextProtocol.Core/Server/McpServerImpl.cs
@@ -1009,11 +1009,15 @@ private async ValueTask ExecuteToolAsTaskAsync(
// Invoke the tool with task-specific cancellation token
var result = await tool.InvokeAsync(request, taskCancellationToken).ConfigureAwait(false);
- ToolCallCompleted(request.Params?.Name ?? string.Empty);
// Determine final status based on whether there was an error
var finalStatus = result.IsError is true ? McpTaskStatus.Failed : McpTaskStatus.Completed;
+ if (finalStatus == McpTaskStatus.Completed)
+ {
+ ToolCallCompleted(request.Params?.Name ?? string.Empty);
+ }
+
// Store the result (serialize to JsonElement)
var resultElement = JsonSerializer.SerializeToElement(result, McpJsonUtilities.JsonContext.Default.CallToolResult);
var finalTask = await taskStore.StoreTaskResultAsync(
From c18f4067e13d00dbd60baee7ac8429fd58495b58 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 10 Feb 2026 03:05:58 +0000
Subject: [PATCH 4/6] Include IsError in ToolCallCompleted log, add task-path
tests
Address reviewer feedback:
- Include CallToolResult.IsError as a parameter in the ToolCallCompleted
log message (@halter73)
- Always log ToolCallCompleted when InvokeAsync doesn't throw, including
when IsError=true (@halter73)
- Skip ToolCallCompleted in outer handler for task-augmented calls to
avoid double-logging; the async ExecuteToolAsTaskAsync logs separately
- Add ReturnIsError tool for testing IsError=true without throwing
- Add Logs_Tool_Name_With_IsError_When_Tool_Returns_Error test
- Add TaskPath_Logs_Tool_Name_On_Successful_Call test
- Add TaskPath_Logs_Tool_Name_With_IsError_When_Tool_Returns_Error test
- Add TaskPath_Logs_Error_When_Tool_Throws test
- Update tool count assertions for new ReturnIsError tool
Co-authored-by: halter73 <54385+halter73@users.noreply.github.com>
---
.../Server/McpServerImpl.cs | 19 +--
.../McpServerBuilderExtensionsToolsTests.cs | 39 +++++-
.../Server/ToolTaskSupportTests.cs | 123 ++++++++++++++++++
3 files changed, 167 insertions(+), 14 deletions(-)
diff --git a/src/ModelContextProtocol.Core/Server/McpServerImpl.cs b/src/ModelContextProtocol.Core/Server/McpServerImpl.cs
index ace30500a..a9099ebd7 100644
--- a/src/ModelContextProtocol.Core/Server/McpServerImpl.cs
+++ b/src/ModelContextProtocol.Core/Server/McpServerImpl.cs
@@ -611,7 +611,14 @@ await originalListToolsHandler(request, cancellationToken).ConfigureAwait(false)
try
{
var result = await handler(request, cancellationToken).ConfigureAwait(false);
- ToolCallCompleted(request.Params?.Name ?? string.Empty);
+
+ // Don't log here for task-augmented calls; logging happens asynchronously
+ // in ExecuteToolAsTaskAsync when the tool actually completes.
+ if (result.Task is null)
+ {
+ ToolCallCompleted(request.Params?.Name ?? string.Empty, result.IsError is true);
+ }
+
return result;
}
catch (Exception e) when (e is not OperationCanceledException and not McpProtocolException)
@@ -946,8 +953,8 @@ internal static LoggingLevel ToLoggingLevel(LogLevel level) =>
[LoggerMessage(Level = LogLevel.Error, Message = "\"{ToolName}\" threw an unhandled exception.")]
private partial void ToolCallError(string toolName, Exception exception);
- [LoggerMessage(Level = LogLevel.Information, Message = "\"{ToolName}\" completed successfully.")]
- private partial void ToolCallCompleted(string toolName);
+ [LoggerMessage(Level = LogLevel.Information, Message = "\"{ToolName}\" completed. IsError = {IsError}.")]
+ private partial void ToolCallCompleted(string toolName, bool isError);
///
/// Executes a tool call as a task and returns a CallToolTaskResult immediately.
@@ -1009,15 +1016,11 @@ private async ValueTask ExecuteToolAsTaskAsync(
// Invoke the tool with task-specific cancellation token
var result = await tool.InvokeAsync(request, taskCancellationToken).ConfigureAwait(false);
+ ToolCallCompleted(request.Params?.Name ?? string.Empty, result.IsError is true);
// Determine final status based on whether there was an error
var finalStatus = result.IsError is true ? McpTaskStatus.Failed : McpTaskStatus.Completed;
- if (finalStatus == McpTaskStatus.Completed)
- {
- ToolCallCompleted(request.Params?.Name ?? string.Empty);
- }
-
// Store the result (serialize to JsonElement)
var resultElement = JsonSerializer.SerializeToElement(result, McpJsonUtilities.JsonContext.Default.CallToolResult);
var finalTask = await taskStore.StoreTaskResultAsync(
diff --git a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsToolsTests.cs b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsToolsTests.cs
index f3972e6fb..734afddb9 100644
--- a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsToolsTests.cs
+++ b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsToolsTests.cs
@@ -127,7 +127,7 @@ public async Task Can_List_Registered_Tools()
await using McpClient client = await CreateMcpClientForServer();
var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
- Assert.Equal(16, tools.Count);
+ Assert.Equal(17, tools.Count);
McpClientTool echoTool = tools.First(t => t.Name == "echo");
Assert.Equal("Echoes the input back to the client.", echoTool.Description);
@@ -165,7 +165,7 @@ public async Task Can_Create_Multiple_Servers_From_Options_And_List_Registered_T
cancellationToken: TestContext.Current.CancellationToken))
{
var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
- Assert.Equal(16, tools.Count);
+ Assert.Equal(17, tools.Count);
McpClientTool echoTool = tools.First(t => t.Name == "echo");
Assert.Equal("Echoes the input back to the client.", echoTool.Description);
@@ -191,7 +191,7 @@ public async Task Can_Be_Notified_Of_Tool_Changes()
await using McpClient client = await CreateMcpClientForServer();
var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
- Assert.Equal(16, tools.Count);
+ Assert.Equal(17, tools.Count);
Channel listChanged = Channel.CreateUnbounded();
var notificationRead = listChanged.Reader.ReadAsync(TestContext.Current.CancellationToken);
@@ -212,7 +212,7 @@ public async Task Can_Be_Notified_Of_Tool_Changes()
await notificationRead;
tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
- Assert.Equal(17, tools.Count);
+ Assert.Equal(18, tools.Count);
Assert.Contains(tools, t => t.Name == "NewTool");
notificationRead = listChanged.Reader.ReadAsync(TestContext.Current.CancellationToken);
@@ -222,7 +222,7 @@ public async Task Can_Be_Notified_Of_Tool_Changes()
}
tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
- Assert.Equal(16, tools.Count);
+ Assert.Equal(17, tools.Count);
Assert.DoesNotContain(tools, t => t.Name == "NewTool");
}
@@ -393,7 +393,23 @@ public async Task Logs_Tool_Name_On_Successful_Call()
Assert.True(result.IsError is not true);
Assert.Equal("hello test", (result.Content[0] as TextContentBlock)?.Text);
- var infoLog = Assert.Single(MockLoggerProvider.LogMessages, m => m.Message == "\"echo\" completed successfully.");
+ var infoLog = Assert.Single(MockLoggerProvider.LogMessages, m => m.Message == "\"echo\" completed. IsError = False.");
+ Assert.Equal(LogLevel.Information, infoLog.LogLevel);
+ }
+
+ [Fact]
+ public async Task Logs_Tool_Name_With_IsError_When_Tool_Returns_Error()
+ {
+ await using McpClient client = await CreateMcpClientForServer();
+
+ var result = await client.CallToolAsync(
+ "return_is_error",
+ cancellationToken: TestContext.Current.CancellationToken);
+
+ Assert.True(result.IsError);
+ Assert.Contains("Tool returned an error", (result.Content[0] as TextContentBlock)?.Text);
+
+ var infoLog = Assert.Single(MockLoggerProvider.LogMessages, m => m.Message == "\"return_is_error\" completed. IsError = True.");
Assert.Equal(LogLevel.Information, infoLog.LogLevel);
}
@@ -803,6 +819,16 @@ public static string ThrowException()
throw new InvalidOperationException("Test error");
}
+ [McpServerTool]
+ public static CallToolResult ReturnIsError()
+ {
+ return new CallToolResult
+ {
+ IsError = true,
+ Content = [new TextContentBlock { Text = "Tool returned an error" }],
+ };
+ }
+
[McpServerTool]
public static int ReturnCancellationToken(CancellationToken cancellationToken)
{
@@ -885,5 +911,6 @@ public class ComplexObject
[JsonSerializable(typeof(ComplexObject))]
[JsonSerializable(typeof(string[]))]
[JsonSerializable(typeof(JsonElement))]
+ [JsonSerializable(typeof(CallToolResult))]
partial class BuilderToolsJsonContext : JsonSerializerContext;
}
\ No newline at end of file
diff --git a/tests/ModelContextProtocol.Tests/Server/ToolTaskSupportTests.cs b/tests/ModelContextProtocol.Tests/Server/ToolTaskSupportTests.cs
index 0bf70b7c2..74587119f 100644
--- a/tests/ModelContextProtocol.Tests/Server/ToolTaskSupportTests.cs
+++ b/tests/ModelContextProtocol.Tests/Server/ToolTaskSupportTests.cs
@@ -530,6 +530,129 @@ public async Task SyncTool_WithRequiredTaskSupport_CannotBeCalledDirectly()
Assert.Equal(McpErrorCode.MethodNotFound, exception.ErrorCode);
Assert.Contains("task", exception.Message, StringComparison.OrdinalIgnoreCase);
}
+
+ [Fact]
+ public async Task TaskPath_Logs_Tool_Name_On_Successful_Call()
+ {
+ var taskStore = new InMemoryMcpTaskStore();
+
+ await using var fixture = new ClientServerFixture(
+ LoggerFactory,
+ configureServer: builder =>
+ {
+ builder.WithTools([McpServerTool.Create(
+ (string input) => $"Result: {input}",
+ new McpServerToolCreateOptions
+ {
+ Name = "task-success-tool",
+ Execution = new ToolExecution { TaskSupport = ToolTaskSupport.Optional }
+ })]);
+ },
+ configureServices: services =>
+ {
+ services.AddSingleton(MockLoggerProvider);
+ services.AddSingleton(taskStore);
+ services.Configure(options => options.TaskStore = taskStore);
+ });
+
+ var mcpTask = await fixture.Client.CallToolAsTaskAsync(
+ "task-success-tool",
+ arguments: new Dictionary { ["input"] = "test" },
+ taskMetadata: new McpTaskMetadata(),
+ progress: null,
+ cancellationToken: TestContext.Current.CancellationToken);
+
+ Assert.NotNull(mcpTask);
+
+ // Wait briefly for the async task execution to complete
+ await Task.Delay(500, TestContext.Current.CancellationToken);
+
+ var infoLog = Assert.Single(MockLoggerProvider.LogMessages, m => m.Message == "\"task-success-tool\" completed. IsError = False.");
+ Assert.Equal(LogLevel.Information, infoLog.LogLevel);
+ }
+
+ [Fact]
+ public async Task TaskPath_Logs_Tool_Name_With_IsError_When_Tool_Returns_Error()
+ {
+ var taskStore = new InMemoryMcpTaskStore();
+
+ await using var fixture = new ClientServerFixture(
+ LoggerFactory,
+ configureServer: builder =>
+ {
+ builder.WithTools([McpServerTool.Create(
+ () => new CallToolResult
+ {
+ IsError = true,
+ Content = [new TextContentBlock { Text = "Task tool error" }],
+ },
+ new McpServerToolCreateOptions
+ {
+ Name = "task-error-result-tool",
+ Execution = new ToolExecution { TaskSupport = ToolTaskSupport.Optional }
+ })]);
+ },
+ configureServices: services =>
+ {
+ services.AddSingleton(MockLoggerProvider);
+ services.AddSingleton(taskStore);
+ services.Configure(options => options.TaskStore = taskStore);
+ });
+
+ var mcpTask = await fixture.Client.CallToolAsTaskAsync(
+ "task-error-result-tool",
+ taskMetadata: new McpTaskMetadata(),
+ progress: null,
+ cancellationToken: TestContext.Current.CancellationToken);
+
+ Assert.NotNull(mcpTask);
+
+ // Wait briefly for the async task execution to complete
+ await Task.Delay(500, TestContext.Current.CancellationToken);
+
+ var infoLog = Assert.Single(MockLoggerProvider.LogMessages, m => m.Message == "\"task-error-result-tool\" completed. IsError = True.");
+ Assert.Equal(LogLevel.Information, infoLog.LogLevel);
+ }
+
+ [Fact]
+ public async Task TaskPath_Logs_Error_When_Tool_Throws()
+ {
+ var taskStore = new InMemoryMcpTaskStore();
+
+ await using var fixture = new ClientServerFixture(
+ LoggerFactory,
+ configureServer: builder =>
+ {
+ builder.WithTools([McpServerTool.Create(
+ string () => throw new InvalidOperationException("Task tool error"),
+ new McpServerToolCreateOptions
+ {
+ Name = "task-throw-tool",
+ Execution = new ToolExecution { TaskSupport = ToolTaskSupport.Optional }
+ })]);
+ },
+ configureServices: services =>
+ {
+ services.AddSingleton(MockLoggerProvider);
+ services.AddSingleton(taskStore);
+ services.Configure(options => options.TaskStore = taskStore);
+ });
+
+ var mcpTask = await fixture.Client.CallToolAsTaskAsync(
+ "task-throw-tool",
+ taskMetadata: new McpTaskMetadata(),
+ progress: null,
+ cancellationToken: TestContext.Current.CancellationToken);
+
+ Assert.NotNull(mcpTask);
+
+ // Wait briefly for the async task execution to complete
+ await Task.Delay(500, TestContext.Current.CancellationToken);
+
+ var errorLog = Assert.Single(MockLoggerProvider.LogMessages, m => m.LogLevel == LogLevel.Error);
+ Assert.Equal("\"task-throw-tool\" threw an unhandled exception.", errorLog.Message);
+ Assert.IsType(errorLog.Exception);
+ }
#pragma warning restore MCPEXP001
#endregion
From 515db785af325c4b9276d6bbdccc03e4cbd55d8d Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 10 Feb 2026 03:44:16 +0000
Subject: [PATCH 5/6] Replace Task.Delay with GetTaskResultAsync in task-path
tests
Use GetTaskResultAsync to wait for task completion instead of arbitrary
Task.Delay(500) calls, making tests deterministic and faster.
Co-authored-by: halter73 <54385+halter73@users.noreply.github.com>
---
.../Server/ToolTaskSupportTests.cs | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/tests/ModelContextProtocol.Tests/Server/ToolTaskSupportTests.cs b/tests/ModelContextProtocol.Tests/Server/ToolTaskSupportTests.cs
index 74587119f..99c6035e6 100644
--- a/tests/ModelContextProtocol.Tests/Server/ToolTaskSupportTests.cs
+++ b/tests/ModelContextProtocol.Tests/Server/ToolTaskSupportTests.cs
@@ -564,8 +564,8 @@ public async Task TaskPath_Logs_Tool_Name_On_Successful_Call()
Assert.NotNull(mcpTask);
- // Wait briefly for the async task execution to complete
- await Task.Delay(500, TestContext.Current.CancellationToken);
+ // Wait for the async task execution to complete
+ await fixture.Client.GetTaskResultAsync(mcpTask.TaskId, cancellationToken: TestContext.Current.CancellationToken);
var infoLog = Assert.Single(MockLoggerProvider.LogMessages, m => m.Message == "\"task-success-tool\" completed. IsError = False.");
Assert.Equal(LogLevel.Information, infoLog.LogLevel);
@@ -607,8 +607,8 @@ public async Task TaskPath_Logs_Tool_Name_With_IsError_When_Tool_Returns_Error()
Assert.NotNull(mcpTask);
- // Wait briefly for the async task execution to complete
- await Task.Delay(500, TestContext.Current.CancellationToken);
+ // Wait for the async task execution to complete
+ await fixture.Client.GetTaskResultAsync(mcpTask.TaskId, cancellationToken: TestContext.Current.CancellationToken);
var infoLog = Assert.Single(MockLoggerProvider.LogMessages, m => m.Message == "\"task-error-result-tool\" completed. IsError = True.");
Assert.Equal(LogLevel.Information, infoLog.LogLevel);
@@ -646,8 +646,8 @@ public async Task TaskPath_Logs_Error_When_Tool_Throws()
Assert.NotNull(mcpTask);
- // Wait briefly for the async task execution to complete
- await Task.Delay(500, TestContext.Current.CancellationToken);
+ // Wait for the async task execution to complete
+ await fixture.Client.GetTaskResultAsync(mcpTask.TaskId, cancellationToken: TestContext.Current.CancellationToken);
var errorLog = Assert.Single(MockLoggerProvider.LogMessages, m => m.LogLevel == LogLevel.Error);
Assert.Equal("\"task-throw-tool\" threw an unhandled exception.", errorLog.Message);
From d250e0dc710d02e18bba8bf20f6750c624f41c7c Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 10 Feb 2026 03:52:23 +0000
Subject: [PATCH 6/6] Add logging for prompts and resources (success and error)
Add GetPromptCompleted/GetPromptError and ReadResourceCompleted/
ReadResourceError LoggerMessage methods, symmetric with the existing
tool logging. Wrap prompt and resource filter pipelines with try/catch
to log when handlers succeed or throw unhandled exceptions.
Add tests:
- Logs_Prompt_Name_On_Successful_Call
- Logs_Prompt_Name_When_Prompt_Throws
- Logs_Resource_Uri_On_Successful_Read
- Logs_Resource_Uri_When_Resource_Throws
Co-authored-by: halter73 <54385+halter73@users.noreply.github.com>
---
.../Server/McpServerImpl.cs | 38 +++++++++++++++++--
.../McpServerBuilderExtensionsPromptsTests.cs | 32 ++++++++++++++++
...cpServerBuilderExtensionsResourcesTests.cs | 30 +++++++++++++++
3 files changed, 97 insertions(+), 3 deletions(-)
diff --git a/src/ModelContextProtocol.Core/Server/McpServerImpl.cs b/src/ModelContextProtocol.Core/Server/McpServerImpl.cs
index a9099ebd7..0165db0e0 100644
--- a/src/ModelContextProtocol.Core/Server/McpServerImpl.cs
+++ b/src/ModelContextProtocol.Core/Server/McpServerImpl.cs
@@ -392,7 +392,17 @@ await originalListResourceTemplatesHandler(request, cancellationToken).Configure
}
}
- return await handler(request, cancellationToken).ConfigureAwait(false);
+ try
+ {
+ var result = await handler(request, cancellationToken).ConfigureAwait(false);
+ ReadResourceCompleted(request.Params?.Uri ?? string.Empty);
+ return result;
+ }
+ catch (Exception e) when (e is not OperationCanceledException and not McpProtocolException)
+ {
+ ReadResourceError(request.Params?.Uri ?? string.Empty, e);
+ throw;
+ }
});
subscribeHandler = BuildFilterPipeline(subscribeHandler, options.Filters.SubscribeToResourcesFilters);
unsubscribeHandler = BuildFilterPipeline(unsubscribeHandler, options.Filters.UnsubscribeFromResourcesFilters);
@@ -487,7 +497,7 @@ await originalListPromptsHandler(request, cancellationToken).ConfigureAwait(fals
listPromptsHandler = BuildFilterPipeline(listPromptsHandler, options.Filters.ListPromptsFilters);
getPromptHandler = BuildFilterPipeline(getPromptHandler, options.Filters.GetPromptFilters, handler =>
- (request, cancellationToken) =>
+ async (request, cancellationToken) =>
{
// Initial handler that sets MatchedPrimitive
if (request.Params?.Name is { } promptName && prompts is not null &&
@@ -496,7 +506,17 @@ await originalListPromptsHandler(request, cancellationToken).ConfigureAwait(fals
request.MatchedPrimitive = prompt;
}
- return handler(request, cancellationToken);
+ try
+ {
+ var result = await handler(request, cancellationToken).ConfigureAwait(false);
+ GetPromptCompleted(request.Params?.Name ?? string.Empty);
+ return result;
+ }
+ catch (Exception e) when (e is not OperationCanceledException and not McpProtocolException)
+ {
+ GetPromptError(request.Params?.Name ?? string.Empty, e);
+ throw;
+ }
});
ServerCapabilities.Prompts.ListChanged = listChanged;
@@ -956,6 +976,18 @@ internal static LoggingLevel ToLoggingLevel(LogLevel level) =>
[LoggerMessage(Level = LogLevel.Information, Message = "\"{ToolName}\" completed. IsError = {IsError}.")]
private partial void ToolCallCompleted(string toolName, bool isError);
+ [LoggerMessage(Level = LogLevel.Error, Message = "GetPrompt \"{PromptName}\" threw an unhandled exception.")]
+ private partial void GetPromptError(string promptName, Exception exception);
+
+ [LoggerMessage(Level = LogLevel.Information, Message = "GetPrompt \"{PromptName}\" completed.")]
+ private partial void GetPromptCompleted(string promptName);
+
+ [LoggerMessage(Level = LogLevel.Error, Message = "ReadResource \"{ResourceUri}\" threw an unhandled exception.")]
+ private partial void ReadResourceError(string resourceUri, Exception exception);
+
+ [LoggerMessage(Level = LogLevel.Information, Message = "ReadResource \"{ResourceUri}\" completed.")]
+ private partial void ReadResourceCompleted(string resourceUri);
+
///
/// Executes a tool call as a task and returns a CallToolTaskResult immediately.
///
diff --git a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsPromptsTests.cs b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsPromptsTests.cs
index 3b9137f61..59ab9916d 100644
--- a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsPromptsTests.cs
+++ b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsPromptsTests.cs
@@ -1,5 +1,6 @@
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using ModelContextProtocol.Client;
using ModelContextProtocol.Protocol;
@@ -195,6 +196,37 @@ await Assert.ThrowsAsync(async () => await client.GetPromp
cancellationToken: TestContext.Current.CancellationToken));
}
+ [Fact]
+ public async Task Logs_Prompt_Name_On_Successful_Call()
+ {
+ await using McpClient client = await CreateMcpClientForServer();
+
+ var result = await client.GetPromptAsync(
+ "returns_chat_messages",
+ new Dictionary { ["message"] = "hello" },
+ cancellationToken: TestContext.Current.CancellationToken);
+
+ Assert.NotNull(result);
+
+ var infoLog = Assert.Single(MockLoggerProvider.LogMessages, m => m.Message == "GetPrompt \"returns_chat_messages\" completed.");
+ Assert.Equal(LogLevel.Information, infoLog.LogLevel);
+ }
+
+ [Fact]
+ public async Task Logs_Prompt_Name_When_Prompt_Throws()
+ {
+ await using McpClient client = await CreateMcpClientForServer();
+
+ await Assert.ThrowsAsync(async () => await client.GetPromptAsync(
+ "throws_exception",
+ new Dictionary { ["message"] = "test" },
+ cancellationToken: TestContext.Current.CancellationToken));
+
+ var errorLog = Assert.Single(MockLoggerProvider.LogMessages, m => m.LogLevel == LogLevel.Error);
+ Assert.Equal("GetPrompt \"throws_exception\" threw an unhandled exception.", errorLog.Message);
+ Assert.IsType(errorLog.Exception);
+ }
+
[Fact]
public async Task Throws_Exception_On_Unknown_Prompt()
{
diff --git a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsResourcesTests.cs b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsResourcesTests.cs
index 5d3f56233..30e10b99a 100644
--- a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsResourcesTests.cs
+++ b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsResourcesTests.cs
@@ -1,5 +1,6 @@
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using ModelContextProtocol.Client;
using ModelContextProtocol.Protocol;
@@ -239,6 +240,35 @@ await Assert.ThrowsAsync(async () => await client.ReadReso
cancellationToken: TestContext.Current.CancellationToken));
}
+ [Fact]
+ public async Task Logs_Resource_Uri_On_Successful_Read()
+ {
+ await using McpClient client = await CreateMcpClientForServer();
+
+ var result = await client.ReadResourceAsync(
+ "resource://mcp/some_neat_direct_resource",
+ cancellationToken: TestContext.Current.CancellationToken);
+
+ Assert.NotNull(result);
+
+ var infoLog = Assert.Single(MockLoggerProvider.LogMessages, m => m.Message == "ReadResource \"resource://mcp/some_neat_direct_resource\" completed.");
+ Assert.Equal(LogLevel.Information, infoLog.LogLevel);
+ }
+
+ [Fact]
+ public async Task Logs_Resource_Uri_When_Resource_Throws()
+ {
+ await using McpClient client = await CreateMcpClientForServer();
+
+ await Assert.ThrowsAsync(async () => await client.ReadResourceAsync(
+ "resource://mcp/throws_exception",
+ cancellationToken: TestContext.Current.CancellationToken));
+
+ var errorLog = Assert.Single(MockLoggerProvider.LogMessages, m => m.LogLevel == LogLevel.Error);
+ Assert.Equal("ReadResource \"resource://mcp/throws_exception\" threw an unhandled exception.", errorLog.Message);
+ Assert.IsType(errorLog.Exception);
+ }
+
[Fact]
public async Task Throws_Exception_On_Unknown_Resource()
{