Clean up (use of) McpException and McpTransportException (#321)

* Clean up (use of) McpException

* Replace McpTransportException with InvalidOperationException

* Add cancellation test
This commit is contained in:
Stephen Toub 2025-04-18 12:25:29 -04:00 • committed by GitHub
commit 7d77be68cf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 231 additions and 219 deletions

View file

@ -5,6 +5,7 @@ using ModelContextProtocol.Protocol.Transport;
using ModelContextProtocol.Protocol.Types;
using ModelContextProtocol.Utils.Json;
using Moq;
using System.IO.Pipelines;
using System.Text.Json;
using System.Threading.Channels;
@ -29,6 +30,31 @@ public class McpClientFactoryTests
Assert.NotNull(client);
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public async Task Cancellation_ThrowsCancellationException(bool preCanceled)
{
var cts = new CancellationTokenSource();
if (preCanceled)
{
cts.Cancel();
}
Task t = McpClientFactory.CreateAsync(
new StreamClientTransport(new Pipe().Writer.AsStream(), new Pipe().Reader.AsStream()),
cancellationToken: cts.Token);
Assert.False(t.IsCompleted);
if (!preCanceled)
{
cts.Cancel();
}
await Assert.ThrowsAnyAsync<OperationCanceledException>(() => t);
}
[Theory]
[InlineData(typeof(NopTransport))]
[InlineData(typeof(FailureTransport))]

View file

@ -31,10 +31,10 @@ public partial class McpServerBuilderExtensionsPromptsTests : ClientServerTestBa
{
NextCursor = "abc",
Prompts = [new()
{
Name = "FirstCustomPrompt",
Description = "First prompt returned by custom handler",
}],
{
Name = "FirstCustomPrompt",
Description = "First prompt returned by custom handler",
}],
};
case "abc":
@ -42,10 +42,10 @@ public partial class McpServerBuilderExtensionsPromptsTests : ClientServerTestBa
{
NextCursor = "def",
Prompts = [new()
{
Name = "SecondCustomPrompt",
Description = "Second prompt returned by custom handler",
}],
{
Name = "SecondCustomPrompt",
Description = "Second prompt returned by custom handler",
}],
};
case "def":
@ -53,14 +53,14 @@ public partial class McpServerBuilderExtensionsPromptsTests : ClientServerTestBa
{
NextCursor = null,
Prompts = [new()
{
Name = "FinalCustomPrompt",
Description = "Final prompt returned by custom handler",
}],
{
Name = "FinalCustomPrompt",
Description = "Final prompt returned by custom handler",
}],
};
default:
throw new Exception("Unexpected cursor");
throw new McpException($"Unexpected cursor: '{cursor}'", McpErrorCode.InvalidParams);
}
})
.WithGetPromptHandler(async (request, cancellationToken) =>
@ -76,7 +76,7 @@ public partial class McpServerBuilderExtensionsPromptsTests : ClientServerTestBa
};
default:
throw new Exception($"Unknown prompt '{request.Params?.Name}'");
throw new McpException($"Unknown prompt '{request.Params?.Name}'", McpErrorCode.InvalidParams);
}
})
.WithPrompts<SimplePrompts>();
@ -194,7 +194,7 @@ public partial class McpServerBuilderExtensionsPromptsTests : ClientServerTestBa
nameof(SimplePrompts.ReturnsChatMessages),
cancellationToken: TestContext.Current.CancellationToken));
Assert.Contains("Missing required parameter", e.Message);
Assert.Equal(McpErrorCode.InternalError, e.ErrorCode);
}
[Fact]

View file

@ -88,7 +88,7 @@ public partial class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
};
default:
throw new Exception("Unexpected cursor");
throw new McpException($"Unexpected cursor: '{cursor}'", McpErrorCode.InvalidParams);
}
})
.WithCallToolHandler(async (request, cancellationToken) =>
@ -104,7 +104,7 @@ public partial class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
};
default:
throw new Exception($"Unknown tool '{request.Params?.Name}'");
throw new McpException($"Unknown tool: '{request.Params?.Name}'", McpErrorCode.InvalidParams);
}
})
.WithTools<EchoTool>(serializerOptions: BuilderToolsJsonContext.Default.Options);

View file

@ -108,7 +108,7 @@ public class DiagnosticTests
a.Kind == ActivityKind.Client);
Assert.Equal(ActivityStatusCode.Error, doesNotExistToolClient.Status);
Assert.Equal("-32603", doesNotExistToolClient.Tags.Single(t => t.Key == "rpc.jsonrpc.error_code").Value);
Assert.Equal("-32602", doesNotExistToolClient.Tags.Single(t => t.Key == "rpc.jsonrpc.error_code").Value);
var doesNotExistToolServer = Assert.Single(activities, a =>
a.Tags.Any(t => t.Key == "mcp.tool.name" && t.Value == "does-not-exist") &&
@ -117,7 +117,7 @@ public class DiagnosticTests
a.Kind == ActivityKind.Server);
Assert.Equal(ActivityStatusCode.Error, doesNotExistToolServer.Status);
Assert.Equal("-32603", doesNotExistToolClient.Tags.Single(t => t.Key == "rpc.jsonrpc.error_code").Value);
Assert.Equal("-32602", doesNotExistToolClient.Tags.Single(t => t.Key == "rpc.jsonrpc.error_code").Value);
}
private static async Task RunConnected(Func<IMcpClient, IMcpServer, Task> action)

View file

@ -59,7 +59,7 @@ public class McpServerPromptTests
Assert.Contains("something", prompt.ProtocolPrompt.Arguments?.Select(a => a.Name) ?? []);
Assert.DoesNotContain("actualMyService", prompt.ProtocolPrompt.Arguments?.Select(a => a.Name) ?? []);
await Assert.ThrowsAsync<ArgumentException>(async () => await prompt.GetAsync(
await Assert.ThrowsAsync<InvalidOperationException>(async () => await prompt.GetAsync(
new RequestContext<GetPromptRequestParams>(new Mock<IMcpServer>().Object),
TestContext.Current.CancellationToken));

View file

@ -95,6 +95,6 @@ public class McpServerResourceTests
};
});
var sp = services.BuildServiceProvider();
Assert.Throws<McpException>(() => sp.GetRequiredService<IMcpServer>());
Assert.Throws<InvalidOperationException>(sp.GetRequiredService<IMcpServer>);
}
}

View file

@ -94,7 +94,7 @@ public class McpServerTests : LoggedTest
}
[Fact]
public async Task RequestSamplingAsync_Should_Throw_McpException_If_Client_Does_Not_Support_Sampling()
public async Task RequestSamplingAsync_Should_Throw_Exception_If_Client_Does_Not_Support_Sampling()
{
// Arrange
await using var transport = new TestServerTransport();
@ -104,7 +104,7 @@ public class McpServerTests : LoggedTest
var action = () => server.RequestSamplingAsync(new CreateMessageRequestParams { Messages = [] }, CancellationToken.None);
// Act & Assert
await Assert.ThrowsAsync<ArgumentException>("server", action);
await Assert.ThrowsAsync<InvalidOperationException>(action);
}
[Fact]
@ -130,7 +130,7 @@ public class McpServerTests : LoggedTest
}
[Fact]
public async Task RequestRootsAsync_Should_Throw_McpException_If_Client_Does_Not_Support_Roots()
public async Task RequestRootsAsync_Should_Throw_Exception_If_Client_Does_Not_Support_Roots()
{
// Arrange
await using var transport = new TestServerTransport();
@ -138,7 +138,7 @@ public class McpServerTests : LoggedTest
SetClientCapabilities(server, new ClientCapabilities());
// Act & Assert
await Assert.ThrowsAsync<ArgumentException>("server", () => server.RequestRootsAsync(new ListRootsRequestParams(), CancellationToken.None));
await Assert.ThrowsAsync<InvalidOperationException>(() => server.RequestRootsAsync(new ListRootsRequestParams(), CancellationToken.None));
}
[Fact]
@ -507,7 +507,7 @@ public class McpServerTests : LoggedTest
await using var transport = new TestServerTransport();
var options = CreateOptions(serverCapabilities);
Assert.Throws<McpException>(() => McpServerFactory.Create(transport, options, LoggerFactory));
Assert.Throws<InvalidOperationException>(() => McpServerFactory.Create(transport, options, LoggerFactory));
}
[Fact]
@ -515,7 +515,7 @@ public class McpServerTests : LoggedTest
{
await using var server = new TestServerForIChatClient(supportsSampling: false);
Assert.Throws<ArgumentException>("server", () => server.AsSamplingChatClient());
Assert.Throws<InvalidOperationException>(server.AsSamplingChatClient);
}
[Fact]