Log tool invocation errors (#447)

* Log tool errors
* add test and implement source generated logging
This commit is contained in:
кɵɵѕнī 2025-05-23 16:48:47 -05:00 • committed by GitHub
commit adb2098e4d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 54 additions and 3 deletions

View file

@ -1,5 +1,7 @@
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using ModelContextProtocol.Protocol;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
@ -9,8 +11,10 @@ using System.Text.Json;
namespace ModelContextProtocol.Server;
/// <summary>Provides an <see cref="McpServerTool"/> that's implemented via an <see cref="AIFunction"/>.</summary>
internal sealed class AIFunctionMcpServerTool : McpServerTool
internal sealed partial class AIFunctionMcpServerTool : McpServerTool
{
private readonly ILogger _logger;
/// <summary>
/// Creates an <see cref="McpServerTool"/> instance for a method, specified via a <see cref="Delegate"/> instance.
/// </summary>
@ -194,7 +198,7 @@ internal sealed class AIFunctionMcpServerTool : McpServerTool
}
}
return new AIFunctionMcpServerTool(function, tool);
return new AIFunctionMcpServerTool(function, tool, options?.Services);
}
private static McpServerToolCreateOptions DeriveOptions(MethodInfo method, McpServerToolCreateOptions? options)
@ -239,10 +243,11 @@ internal sealed class AIFunctionMcpServerTool : McpServerTool
internal AIFunction AIFunction { get; }
/// <summary>Initializes a new instance of the <see cref="McpServerTool"/> class.</summary>
private AIFunctionMcpServerTool(AIFunction function, Tool tool)
private AIFunctionMcpServerTool(AIFunction function, Tool tool, IServiceProvider? serviceProvider)
{
AIFunction = function;
ProtocolTool = tool;
_logger = serviceProvider?.GetService<ILoggerFactory>()?.CreateLogger<McpServerTool>() ?? (ILogger)NullLogger.Instance;
}
/// <inheritdoc />
@ -277,6 +282,8 @@ internal sealed class AIFunctionMcpServerTool : McpServerTool
}
catch (Exception e) when (e is not OperationCanceledException)
{
ToolCallError(request.Params?.Name ?? string.Empty, e);
string errorMessage = e is McpException ?
$"An error occurred invoking '{request.Params?.Name}': {e.Message}" :
$"An error occurred invoking '{request.Params?.Name}'.";
@ -359,4 +366,7 @@ internal sealed class AIFunctionMcpServerTool : McpServerTool
IsError = allErrorContent && hasAny
};
}
[LoggerMessage(Level = LogLevel.Error, Message = "\"{ToolName}\" threw an unhandled exception.")]
private partial void ToolCallError(string toolName, Exception exception);
}

View file

@ -1,7 +1,9 @@
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using ModelContextProtocol.Protocol;
using ModelContextProtocol.Server;
using ModelContextProtocol.Tests.Utils;
using Moq;
using System.Reflection;
using System.Text.Json;
@ -381,6 +383,45 @@ public partial class McpServerToolTests
);
}
[Fact]
public async Task ToolCallError_LogsErrorMessage()
{
// Arrange
var mockLoggerProvider = new MockLoggerProvider();
var loggerFactory = new LoggerFactory(new[] { mockLoggerProvider });
var services = new ServiceCollection();
services.AddSingleton<ILoggerFactory>(loggerFactory);
var serviceProvider = services.BuildServiceProvider();
var toolName = "tool-that-throws";
var exceptionMessage = "Test exception message";
McpServerTool tool = McpServerTool.Create(() =>
{
throw new InvalidOperationException(exceptionMessage);
}, new() { Name = toolName, Services = serviceProvider });
var mockServer = new Mock<IMcpServer>();
var request = new RequestContext<CallToolRequestParams>(mockServer.Object)
{
Params = new CallToolRequestParams() { Name = toolName },
Services = serviceProvider
};
// Act
var result = await tool.InvokeAsync(request, TestContext.Current.CancellationToken);
// Assert
Assert.True(result.IsError);
Assert.Single(result.Content);
Assert.Equal($"An error occurred invoking '{toolName}'.", result.Content[0].Text);
var errorLog = Assert.Single(mockLoggerProvider.LogMessages, m => m.LogLevel == LogLevel.Error);
Assert.Equal($"\"{toolName}\" threw an unhandled exception.", errorLog.Message);
Assert.IsType<InvalidOperationException>(errorLog.Exception);
Assert.Equal(exceptionMessage, errorLog.Exception.Message);
}
private sealed class MyService;
private class DisposableToolType : IDisposable