Skip to content

Commit adb2098

Browse files
authored
Log tool invocation errors (#447)
* Log tool errors * add test and implement source generated logging
1 parent 77f9e64 commit adb2098

File tree

2 files changed

+54
-3
lines changed

2 files changed

+54
-3
lines changed

src/ModelContextProtocol/Server/AIFunctionMcpServerTool.cs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
using Microsoft.Extensions.AI;
22
using Microsoft.Extensions.DependencyInjection;
3+
using Microsoft.Extensions.Logging;
4+
using Microsoft.Extensions.Logging.Abstractions;
35
using ModelContextProtocol.Protocol;
46
using System.ComponentModel;
57
using System.Diagnostics.CodeAnalysis;
@@ -9,8 +11,10 @@
911
namespace ModelContextProtocol.Server;
1012

1113
/// <summary>Provides an <see cref="McpServerTool"/> that's implemented via an <see cref="AIFunction"/>.</summary>
12-
internal sealed class AIFunctionMcpServerTool : McpServerTool
14+
internal sealed partial class AIFunctionMcpServerTool : McpServerTool
1315
{
16+
private readonly ILogger _logger;
17+
1418
/// <summary>
1519
/// Creates an <see cref="McpServerTool"/> instance for a method, specified via a <see cref="Delegate"/> instance.
1620
/// </summary>
@@ -194,7 +198,7 @@ options.OpenWorld is not null ||
194198
}
195199
}
196200

197-
return new AIFunctionMcpServerTool(function, tool);
201+
return new AIFunctionMcpServerTool(function, tool, options?.Services);
198202
}
199203

200204
private static McpServerToolCreateOptions DeriveOptions(MethodInfo method, McpServerToolCreateOptions? options)
@@ -239,10 +243,11 @@ private static McpServerToolCreateOptions DeriveOptions(MethodInfo method, McpSe
239243
internal AIFunction AIFunction { get; }
240244

241245
/// <summary>Initializes a new instance of the <see cref="McpServerTool"/> class.</summary>
242-
private AIFunctionMcpServerTool(AIFunction function, Tool tool)
246+
private AIFunctionMcpServerTool(AIFunction function, Tool tool, IServiceProvider? serviceProvider)
243247
{
244248
AIFunction = function;
245249
ProtocolTool = tool;
250+
_logger = serviceProvider?.GetService<ILoggerFactory>()?.CreateLogger<McpServerTool>() ?? (ILogger)NullLogger.Instance;
246251
}
247252

248253
/// <inheritdoc />
@@ -277,6 +282,8 @@ public override async ValueTask<CallToolResponse> InvokeAsync(
277282
}
278283
catch (Exception e) when (e is not OperationCanceledException)
279284
{
285+
ToolCallError(request.Params?.Name ?? string.Empty, e);
286+
280287
string errorMessage = e is McpException ?
281288
$"An error occurred invoking '{request.Params?.Name}': {e.Message}" :
282289
$"An error occurred invoking '{request.Params?.Name}'.";
@@ -359,4 +366,7 @@ private static CallToolResponse ConvertAIContentEnumerableToCallToolResponse(IEn
359366
IsError = allErrorContent && hasAny
360367
};
361368
}
369+
370+
[LoggerMessage(Level = LogLevel.Error, Message = "\"{ToolName}\" threw an unhandled exception.")]
371+
private partial void ToolCallError(string toolName, Exception exception);
362372
}

tests/ModelContextProtocol.Tests/Server/McpServerToolTests.cs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
using Microsoft.Extensions.AI;
22
using Microsoft.Extensions.DependencyInjection;
3+
using Microsoft.Extensions.Logging;
34
using ModelContextProtocol.Protocol;
45
using ModelContextProtocol.Server;
6+
using ModelContextProtocol.Tests.Utils;
57
using Moq;
68
using System.Reflection;
79
using System.Text.Json;
@@ -381,6 +383,45 @@ public async Task SupportsSchemaCreateOptions()
381383
);
382384
}
383385

386+
[Fact]
387+
public async Task ToolCallError_LogsErrorMessage()
388+
{
389+
// Arrange
390+
var mockLoggerProvider = new MockLoggerProvider();
391+
var loggerFactory = new LoggerFactory(new[] { mockLoggerProvider });
392+
var services = new ServiceCollection();
393+
services.AddSingleton<ILoggerFactory>(loggerFactory);
394+
var serviceProvider = services.BuildServiceProvider();
395+
396+
var toolName = "tool-that-throws";
397+
var exceptionMessage = "Test exception message";
398+
399+
McpServerTool tool = McpServerTool.Create(() =>
400+
{
401+
throw new InvalidOperationException(exceptionMessage);
402+
}, new() { Name = toolName, Services = serviceProvider });
403+
404+
var mockServer = new Mock<IMcpServer>();
405+
var request = new RequestContext<CallToolRequestParams>(mockServer.Object)
406+
{
407+
Params = new CallToolRequestParams() { Name = toolName },
408+
Services = serviceProvider
409+
};
410+
411+
// Act
412+
var result = await tool.InvokeAsync(request, TestContext.Current.CancellationToken);
413+
414+
// Assert
415+
Assert.True(result.IsError);
416+
Assert.Single(result.Content);
417+
Assert.Equal($"An error occurred invoking '{toolName}'.", result.Content[0].Text);
418+
419+
var errorLog = Assert.Single(mockLoggerProvider.LogMessages, m => m.LogLevel == LogLevel.Error);
420+
Assert.Equal($"\"{toolName}\" threw an unhandled exception.", errorLog.Message);
421+
Assert.IsType<InvalidOperationException>(errorLog.Exception);
422+
Assert.Equal(exceptionMessage, errorLog.Exception.Message);
423+
}
424+
384425
private sealed class MyService;
385426

386427
private class DisposableToolType : IDisposable

0 commit comments

Comments
 (0)