Propagate tool call exceptions through filters (#844)

* Add McpProtocolException specifically for JSON-RPC errors
This commit is contained in:
Stephen Halter 2025-10-14 21:10:55 -05:00 • committed by GitHub
commit 3255adb4d6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 334 additions and 163 deletions

View file

@ -36,7 +36,7 @@ public class AuthorizeAttributeTests(ITestOutputHelper testOutputHelper) : Kestr
var client = await ConnectAsync();
var exception = await Assert.ThrowsAsync<McpException>(async () =>
var exception = await Assert.ThrowsAsync<McpProtocolException>(async () =>
await client.CallToolAsync(
"authorized_tool",
new Dictionary<string, object?> { ["message"] = "test" },
@ -101,7 +101,7 @@ public class AuthorizeAttributeTests(ITestOutputHelper testOutputHelper) : Kestr
var client = await ConnectAsync();
var exception = await Assert.ThrowsAsync<McpException>(async () =>
var exception = await Assert.ThrowsAsync<McpProtocolException>(async () =>
await client.CallToolAsync(
"admin_tool",
new Dictionary<string, object?> { ["message"] = "test" },
@ -188,7 +188,7 @@ public class AuthorizeAttributeTests(ITestOutputHelper testOutputHelper) : Kestr
var client = await ConnectAsync();
var exception = await Assert.ThrowsAsync<McpException>(async () =>
var exception = await Assert.ThrowsAsync<McpProtocolException>(async () =>
await client.GetPromptAsync(
"authorized_prompt",
new Dictionary<string, object?> { ["message"] = "test" },
@ -235,7 +235,7 @@ public class AuthorizeAttributeTests(ITestOutputHelper testOutputHelper) : Kestr
var client = await ConnectAsync();
var exception = await Assert.ThrowsAsync<McpException>(async () =>
var exception = await Assert.ThrowsAsync<McpProtocolException>(async () =>
await client.ReadResourceAsync(
"resource://authorized",
cancellationToken: TestContext.Current.CancellationToken));
@ -277,7 +277,7 @@ public class AuthorizeAttributeTests(ITestOutputHelper testOutputHelper) : Kestr
await using var app = await StartServerWithoutAuthFilters(builder => builder.WithTools<AuthorizationTestTools>());
var client = await ConnectAsync();
var exception = await Assert.ThrowsAsync<McpException>(async () =>
var exception = await Assert.ThrowsAsync<McpProtocolException>(async () =>
await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken));
Assert.Equal("Request failed (remote): An error occurred.", exception.Message);
@ -289,21 +289,23 @@ public class AuthorizeAttributeTests(ITestOutputHelper testOutputHelper) : Kestr
}
[Fact]
public async Task CallTool_WithoutAuthFilters_ThrowsInvalidOperationException()
public async Task CallTool_WithoutAuthFilters_ReturnsError()
{
_mockLoggerProvider.LogMessages.Clear();
await using var app = await StartServerWithoutAuthFilters(builder => builder.WithTools<AuthorizationTestTools>());
var client = await ConnectAsync();
var exception = await Assert.ThrowsAsync<McpException>(async () =>
await client.CallToolAsync(
var toolResult = await client.CallToolAsync(
"authorized_tool",
new Dictionary<string, object?> { ["message"] = "test" },
cancellationToken: TestContext.Current.CancellationToken));
cancellationToken: TestContext.Current.CancellationToken);
Assert.Equal("Request failed (remote): An error occurred.", exception.Message);
Assert.True(toolResult.IsError);
var errorContent = Assert.IsType<TextContentBlock>(Assert.Single(toolResult.Content));
Assert.Equal("An error occurred invoking 'authorized_tool'.", errorContent.Text);
Assert.Contains(_mockLoggerProvider.LogMessages, log =>
log.LogLevel == LogLevel.Warning &&
log.LogLevel == LogLevel.Error &&
log.Exception is InvalidOperationException &&
log.Exception.Message.Contains("Authorization filter was not invoked for tools/call operation") &&
log.Exception.Message.Contains("Ensure that AddAuthorizationFilters() is called"));
@ -316,7 +318,7 @@ public class AuthorizeAttributeTests(ITestOutputHelper testOutputHelper) : Kestr
await using var app = await StartServerWithoutAuthFilters(builder => builder.WithPrompts<AuthorizationTestPrompts>());
var client = await ConnectAsync();
var exception = await Assert.ThrowsAsync<McpException>(async () =>
var exception = await Assert.ThrowsAsync<McpProtocolException>(async () =>
await client.ListPromptsAsync(cancellationToken: TestContext.Current.CancellationToken));
Assert.Equal("Request failed (remote): An error occurred.", exception.Message);
@ -334,7 +336,7 @@ public class AuthorizeAttributeTests(ITestOutputHelper testOutputHelper) : Kestr
await using var app = await StartServerWithoutAuthFilters(builder => builder.WithPrompts<AuthorizationTestPrompts>());
var client = await ConnectAsync();
var exception = await Assert.ThrowsAsync<McpException>(async () =>
var exception = await Assert.ThrowsAsync<McpProtocolException>(async () =>
await client.GetPromptAsync(
"authorized_prompt",
new Dictionary<string, object?> { ["message"] = "test" },
@ -355,7 +357,7 @@ public class AuthorizeAttributeTests(ITestOutputHelper testOutputHelper) : Kestr
await using var app = await StartServerWithoutAuthFilters(builder => builder.WithResources<AuthorizationTestResources>());
var client = await ConnectAsync();
var exception = await Assert.ThrowsAsync<McpException>(async () =>
var exception = await Assert.ThrowsAsync<McpProtocolException>(async () =>
await client.ListResourcesAsync(cancellationToken: TestContext.Current.CancellationToken));
Assert.Equal("Request failed (remote): An error occurred.", exception.Message);
@ -373,7 +375,7 @@ public class AuthorizeAttributeTests(ITestOutputHelper testOutputHelper) : Kestr
await using var app = await StartServerWithoutAuthFilters(builder => builder.WithResources<AuthorizationTestResources>());
var client = await ConnectAsync();
var exception = await Assert.ThrowsAsync<McpException>(async () =>
var exception = await Assert.ThrowsAsync<McpProtocolException>(async () =>
await client.ReadResourceAsync(
"resource://authorized",
cancellationToken: TestContext.Current.CancellationToken));
@ -393,7 +395,7 @@ public class AuthorizeAttributeTests(ITestOutputHelper testOutputHelper) : Kestr
await using var app = await StartServerWithoutAuthFilters(builder => builder.WithResources<AuthorizationTestResources>());
var client = await ConnectAsync();
var exception = await Assert.ThrowsAsync<McpException>(async () =>
var exception = await Assert.ThrowsAsync<McpProtocolException>(async () =>
await client.ListResourceTemplatesAsync(cancellationToken: TestContext.Current.CancellationToken));
Assert.Equal("Request failed (remote): An error occurred.", exception.Message);

View file

@ -240,7 +240,7 @@ public abstract class HttpServerIntegrationTests : LoggedTest, IClassFixture<Sse
// act
await using var client = await GetClientAsync();
await Assert.ThrowsAsync<McpException>(async () => await client.GetPromptAsync("non_existent_prompt", null, cancellationToken: TestContext.Current.CancellationToken));
await Assert.ThrowsAsync<McpProtocolException>(async () => await client.GetPromptAsync("non_existent_prompt", null, cancellationToken: TestContext.Current.CancellationToken));
}
[Fact]

View file

@ -2,6 +2,7 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Primitives;
using ModelContextProtocol.Client;
using System.Collections.Concurrent;
namespace ModelContextProtocol.AspNetCore.Tests;
@ -148,7 +149,7 @@ public class MapMcpStreamableHttpTests(ITestOutputHelper outputHelper) : MapMcpT
[Fact]
public async Task StreamableHttpClient_SendsMcpProtocolVersionHeader_AfterInitialization()
{
var protocolVersionHeaderValues = new List<string?>();
var protocolVersionHeaderValues = new ConcurrentQueue<string?>();
Builder.Services.AddMcpServer().WithHttpTransport(ConfigureStateless).WithTools<EchoHttpContextUserTools>();
@ -160,7 +161,7 @@ public class MapMcpStreamableHttpTests(ITestOutputHelper outputHelper) : MapMcpT
{
if (!StringValues.IsNullOrEmpty(context.Request.Headers["mcp-protocol-version"]))
{
protocolVersionHeaderValues.Add(context.Request.Headers["mcp-protocol-version"]);
protocolVersionHeaderValues.Enqueue(context.Request.Headers["mcp-protocol-version"]);
}
await next(context);