Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion src/ModelContextProtocol.Core/McpSessionHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -186,10 +186,10 @@ ex is OperationCanceledException &&
{
Code = (int)mcpProtocolException.ErrorCode,
Message = mcpProtocolException.Message,
Data = ConvertExceptionData(mcpProtocolException.Data),
} : ex is McpException mcpException ?
new()
{

Code = (int)McpErrorCode.InternalError,
Message = mcpException.Message,
} :
Expand Down Expand Up @@ -769,6 +769,29 @@ private static TimeSpan GetElapsed(long startingTimestamp) =>
return null;
}

/// <summary>
/// Converts the Exception.Data dictionary to a serializable Dictionary&lt;string, object?&gt;.
/// Returns null if the data dictionary is empty.
/// </summary>
private static Dictionary<string, object?>? ConvertExceptionData(System.Collections.IDictionary data)
{
if (data.Count == 0)
{
return null;
}

var result = new Dictionary<string, object?>(data.Count);
foreach (System.Collections.DictionaryEntry entry in data)
{
if (entry.Key is string key)
{
result[key] = entry.Value;
}
}

return result.Count > 0 ? result : null;
}

[LoggerMessage(Level = LogLevel.Information, Message = "{EndpointName} message processing canceled.")]
private partial void LogEndpointMessageProcessingCanceled(string endpointName);

Expand Down
60 changes: 60 additions & 0 deletions tests/ModelContextProtocol.Tests/Server/McpServerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -671,6 +671,66 @@ await transport.SendMessageAsync(
await runTask;
}

[Fact]
public async Task Can_Handle_Call_Tool_Requests_With_McpProtocolException_And_Data()
{
const string errorMessage = "Resource not found";
const McpErrorCode errorCode = (McpErrorCode)(-32002);
const string resourceUri = "file:///path/to/resource";

await using var transport = new TestServerTransport();
var options = CreateOptions(new ServerCapabilities { Tools = new() });
options.Handlers.CallToolHandler = async (request, ct) =>
{
throw new McpProtocolException(errorMessage, errorCode)
{
Data =
{
{ "uri", resourceUri }
}
};
};
options.Handlers.ListToolsHandler = (request, ct) => throw new NotImplementedException();

await using var server = McpServer.Create(transport, options, LoggerFactory);

var runTask = server.RunAsync(TestContext.Current.CancellationToken);

var receivedMessage = new TaskCompletionSource<JsonRpcError>();

transport.OnMessageSent = (message) =>
{
if (message is JsonRpcError error && error.Id.ToString() == "55")
receivedMessage.SetResult(error);
};

await transport.SendMessageAsync(
new JsonRpcRequest
{
Method = RequestMethods.ToolsCall,
Id = new RequestId(55)
},
TestContext.Current.CancellationToken
);

var error = await receivedMessage.Task.WaitAsync(TimeSpan.FromSeconds(10), TestContext.Current.CancellationToken);
Assert.NotNull(error);
Assert.NotNull(error.Error);
Assert.Equal((int)errorCode, error.Error.Code);
Assert.Equal(errorMessage, error.Error.Message);
Assert.NotNull(error.Error.Data);

// Verify the data contains the uri
var dataJson = JsonSerializer.Serialize(error.Error.Data, McpJsonUtilities.DefaultOptions);
var dataObject = JsonSerializer.Deserialize<JsonObject>(dataJson, McpJsonUtilities.DefaultOptions);
Assert.NotNull(dataObject);
Assert.True(dataObject.ContainsKey("uri"));
Assert.Equal(resourceUri, dataObject["uri"]?.GetValue<string>());

await transport.DisposeAsync();
await runTask;
}

private async Task Can_Handle_Requests(ServerCapabilities? serverCapabilities, string method, Action<McpServerOptions>? configureOptions, Action<McpServer, JsonNode?> assertResult)
{
await using var transport = new TestServerTransport();
Expand Down