Deprecate McpErrorCode.ResourceNotFound (-32002) and use McpErrorCode.InvalidParams (-32602) per SEP-2164 (#1558)
This commit is contained in:
parent
712a06b83a
commit
b99d887b46
10 changed files with 77 additions and 15 deletions
|
|
@ -75,4 +75,12 @@ internal static class McpHttpHeaders
|
|||
/// </summary>
|
||||
public static bool SupportsStandardHeaders(string? protocolVersion)
|
||||
=> !string.IsNullOrEmpty(protocolVersion) && s_versionsWithStandardHeaders.Contains(protocolVersion!);
|
||||
|
||||
/// <summary>
|
||||
/// Returns <see langword="true"/> if the negotiated protocol version reports unresolvable
|
||||
/// resource URIs with the standard JSON-RPC <see cref="McpErrorCode.InvalidParams"/> (-32602)
|
||||
/// rather than the legacy <see cref="McpErrorCode.ResourceNotFound"/> (-32002).
|
||||
/// </summary>
|
||||
internal static bool UseInvalidParamsForMissingResource(string? protocolVersion)
|
||||
=> string.Equals(protocolVersion, MinVersionForStandardHeaders, StringComparison.Ordinal);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,8 +29,17 @@ public enum McpErrorCode
|
|||
/// Indicates that the requested resource could not be found.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This error should be used when a resource URI does not match any available resource on the server.
|
||||
/// It allows clients to distinguish between missing resources and other types of errors.
|
||||
/// <para>
|
||||
/// Legacy error code for unresolvable resource URIs. Newer protocol versions report this
|
||||
/// condition with the standard JSON-RPC <see cref="InvalidParams"/> (-32602) instead. The SDK
|
||||
/// selects between the two automatically based on the negotiated protocol version, so older
|
||||
/// clients still see <see cref="ResourceNotFound"/> (-32002) and newer ones see
|
||||
/// <see cref="InvalidParams"/>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// New user code throwing <see cref="McpProtocolException"/> directly for unknown-resource conditions
|
||||
/// should prefer <see cref="InvalidParams"/>; the SDK will pass the value through unchanged.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
ResourceNotFound = -32002,
|
||||
|
||||
|
|
@ -85,6 +94,7 @@ public enum McpErrorCode
|
|||
/// <list type="bullet">
|
||||
/// <item><description><b>Tools</b>: Unknown tool name or invalid protocol-level tool arguments.</description></item>
|
||||
/// <item><description><b>Prompts</b>: Unknown prompt name or missing required protocol-level arguments.</description></item>
|
||||
/// <item><description><b>Resources</b>: Unknown or unresolvable resource URI.</description></item>
|
||||
/// <item><description><b>Pagination</b>: Invalid or expired cursor values.</description></item>
|
||||
/// <item><description><b>Logging</b>: Invalid log level.</description></item>
|
||||
/// <item><description><b>Tasks</b>: Invalid or nonexistent task ID or invalid cursor.</description></item>
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ public class McpProtocolException : McpException
|
|||
/// <item><description>-32700: Parse error - Invalid JSON received</description></item>
|
||||
/// <item><description>-32600: Invalid request - The JSON is not a valid Request object</description></item>
|
||||
/// <item><description>-32601: Method not found - The method does not exist or is not available</description></item>
|
||||
/// <item><description>-32602: Invalid params - Malformed request or unknown primitive name (tool/prompt/resource)</description></item>
|
||||
/// <item><description>-32602: Invalid params - Malformed request, unknown primitive name (tool/prompt/resource), or unresolvable resource URI</description></item>
|
||||
/// <item><description>-32603: Internal error - Internal JSON-RPC error</description></item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
|
|
|
|||
|
|
@ -421,7 +421,13 @@ internal sealed partial class McpServerImpl : McpServer
|
|||
|
||||
listResourcesHandler ??= (static async (_, __) => new ListResourcesResult());
|
||||
listResourceTemplatesHandler ??= (static async (_, __) => new ListResourceTemplatesResult());
|
||||
readResourceHandler ??= (static async (request, _) => throw new McpProtocolException($"Unknown resource URI: '{request.Params?.Uri}'", McpErrorCode.ResourceNotFound));
|
||||
readResourceHandler ??= (static async (request, _) =>
|
||||
{
|
||||
var errorCode = McpHttpHeaders.UseInvalidParamsForMissingResource(request.Server.NegotiatedProtocolVersion)
|
||||
? McpErrorCode.InvalidParams
|
||||
: McpErrorCode.ResourceNotFound;
|
||||
throw new McpProtocolException($"Unknown resource URI: '{request.Params?.Uri}'", errorCode);
|
||||
});
|
||||
subscribeHandler ??= (static async (_, __) => new EmptyResult());
|
||||
unsubscribeHandler ??= (static async (_, __) => new EmptyResult());
|
||||
var listChanged = resourcesCapability?.ListChanged;
|
||||
|
|
|
|||
|
|
@ -503,7 +503,7 @@ internal static class Program
|
|||
}
|
||||
|
||||
ResourceContents contents = resourceContents.FirstOrDefault(r => r.Uri == request.Params.Uri)
|
||||
?? throw new McpProtocolException($"Resource not found: '{request.Params.Uri}'", McpErrorCode.ResourceNotFound);
|
||||
?? throw new McpProtocolException($"Resource not found: '{request.Params.Uri}'", McpErrorCode.InvalidParams);
|
||||
|
||||
return new ReadResourceResult
|
||||
{
|
||||
|
|
|
|||
|
|
@ -307,7 +307,7 @@ public class Program
|
|||
}
|
||||
|
||||
ResourceContents? contents = resourceContents.FirstOrDefault(r => r.Uri == request.Params.Uri) ??
|
||||
throw new McpProtocolException($"Resource not found: '{request.Params.Uri}'", McpErrorCode.ResourceNotFound);
|
||||
throw new McpProtocolException($"Resource not found: '{request.Params.Uri}'", McpErrorCode.InvalidParams);
|
||||
|
||||
return new ReadResourceResult
|
||||
{
|
||||
|
|
|
|||
|
|
@ -109,7 +109,7 @@ public partial class McpServerBuilderExtensionsResourcesTests : ClientServerTest
|
|||
};
|
||||
}
|
||||
|
||||
throw new McpProtocolException($"Resource not found: {request.Params.Uri}", McpErrorCode.ResourceNotFound);
|
||||
throw new McpProtocolException($"Resource not found: {request.Params.Uri}", McpErrorCode.InvalidParams);
|
||||
})
|
||||
.WithResources<SimpleResources>();
|
||||
}
|
||||
|
|
@ -317,7 +317,7 @@ public partial class McpServerBuilderExtensionsResourcesTests : ClientServerTest
|
|||
cancellationToken: TestContext.Current.CancellationToken));
|
||||
|
||||
Assert.Contains("Resource not found", e.Message);
|
||||
Assert.Equal(McpErrorCode.ResourceNotFound, e.ErrorCode);
|
||||
Assert.Equal(McpErrorCode.InvalidParams, e.ErrorCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using ModelContextProtocol.Client;
|
||||
using ModelContextProtocol.Protocol;
|
||||
using ModelContextProtocol.Server;
|
||||
|
|
@ -22,6 +23,23 @@ public sealed class McpServerResourceRoutingTests(ITestOutputHelper testOutputHe
|
|||
return await CreateMcpClientForServer();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts the server with the specified resources, pins both the server's and the
|
||||
/// client's protocol version to <paramref name="protocolVersion"/>, and returns a
|
||||
/// connected client. Both ends must be pinned because <see cref="McpClient"/> strictly
|
||||
/// compares the server's negotiated version against the client's requested version and
|
||||
/// refuses to connect on mismatch.
|
||||
/// </summary>
|
||||
private async Task<McpClient> CreateClientWithResourcesAndServerVersionAsync(
|
||||
string protocolVersion,
|
||||
params McpServerResource[] resources)
|
||||
{
|
||||
McpServerBuilder.WithResources(resources);
|
||||
McpServerBuilder.Services.Configure<McpServerOptions>(o => o.ProtocolVersion = protocolVersion);
|
||||
StartServer();
|
||||
return await CreateMcpClientForServer(new McpClientOptions { ProtocolVersion = protocolVersion });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asserts that the given URI matches the template and produces the expected text result.
|
||||
/// </summary>
|
||||
|
|
@ -56,6 +74,26 @@ public sealed class McpServerResourceRoutingTests(ITestOutputHelper testOutputHe
|
|||
Assert.Equal(McpErrorCode.ResourceNotFound, ex.ErrorCode);
|
||||
}
|
||||
|
||||
// Unknown-resource-URI responses are version-gated: older clients keep the legacy
|
||||
// -32002 (McpErrorCode.ResourceNotFound), and clients on the draft protocol version that
|
||||
// moves to the standard JSON-RPC code see -32602 (McpErrorCode.InvalidParams).
|
||||
[Theory]
|
||||
[InlineData("2025-11-25", McpErrorCode.ResourceNotFound)]
|
||||
[InlineData("DRAFT-2026-v1", McpErrorCode.InvalidParams)]
|
||||
public async Task ResourceNotFound_ErrorCode_IsVersionGated(string serverProtocolVersion, McpErrorCode expectedCode)
|
||||
{
|
||||
var resource = McpServerResource.Create(
|
||||
options: new() { UriTemplate = "test://known/{id}" },
|
||||
method: (string id) => $"ok: {id}");
|
||||
|
||||
var client = await CreateClientWithResourcesAndServerVersionAsync(serverProtocolVersion, resource);
|
||||
|
||||
var ex = await Assert.ThrowsAsync<McpProtocolException>(async () =>
|
||||
await client.ReadResourceAsync("test://unknown", null, TestContext.Current.CancellationToken));
|
||||
|
||||
Assert.Equal(expectedCode, ex.ErrorCode);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verify that when multiple templated resources exist, the correct one is matched based on the URI pattern.
|
||||
/// Regression test for https://github.com/modelcontextprotocol/csharp-sdk/issues/821.
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ public class McpProtocolExceptionDataTests : ClientServerTestBase
|
|||
switch (toolName)
|
||||
{
|
||||
case "throw_with_serializable_data":
|
||||
throw new McpProtocolException("Resource not found", McpErrorCode.ResourceNotFound)
|
||||
throw new McpProtocolException("Resource not found", McpErrorCode.InvalidParams)
|
||||
{
|
||||
Data =
|
||||
{
|
||||
|
|
@ -43,7 +43,7 @@ public class McpProtocolExceptionDataTests : ClientServerTestBase
|
|||
};
|
||||
|
||||
case "throw_with_nonserializable_data":
|
||||
throw new McpProtocolException("Resource not found", McpErrorCode.ResourceNotFound)
|
||||
throw new McpProtocolException("Resource not found", McpErrorCode.InvalidParams)
|
||||
{
|
||||
Data =
|
||||
{
|
||||
|
|
@ -55,7 +55,7 @@ public class McpProtocolExceptionDataTests : ClientServerTestBase
|
|||
};
|
||||
|
||||
case "throw_with_only_nonserializable_data":
|
||||
throw new McpProtocolException("Resource not found", McpErrorCode.ResourceNotFound)
|
||||
throw new McpProtocolException("Resource not found", McpErrorCode.InvalidParams)
|
||||
{
|
||||
Data =
|
||||
{
|
||||
|
|
@ -79,7 +79,7 @@ public class McpProtocolExceptionDataTests : ClientServerTestBase
|
|||
await client.CallToolAsync("throw_with_serializable_data", cancellationToken: TestContext.Current.CancellationToken));
|
||||
|
||||
Assert.Equal("Request failed (remote): Resource not found", exception.Message);
|
||||
Assert.Equal(McpErrorCode.ResourceNotFound, exception.ErrorCode);
|
||||
Assert.Equal(McpErrorCode.InvalidParams, exception.ErrorCode);
|
||||
|
||||
// Verify the data was propagated to the exception
|
||||
// The Data collection should contain the expected keys
|
||||
|
|
@ -113,7 +113,7 @@ public class McpProtocolExceptionDataTests : ClientServerTestBase
|
|||
await client.CallToolAsync("throw_with_nonserializable_data", cancellationToken: TestContext.Current.CancellationToken));
|
||||
|
||||
Assert.Equal("Request failed (remote): Resource not found", exception.Message);
|
||||
Assert.Equal(McpErrorCode.ResourceNotFound, exception.ErrorCode);
|
||||
Assert.Equal(McpErrorCode.InvalidParams, exception.ErrorCode);
|
||||
|
||||
// Verify that only the serializable data was propagated (non-serializable was filtered out)
|
||||
var hasUri = false;
|
||||
|
|
@ -142,7 +142,7 @@ public class McpProtocolExceptionDataTests : ClientServerTestBase
|
|||
await client.CallToolAsync("throw_with_only_nonserializable_data", cancellationToken: TestContext.Current.CancellationToken));
|
||||
|
||||
Assert.Equal("Request failed (remote): Resource not found", exception.Message);
|
||||
Assert.Equal(McpErrorCode.ResourceNotFound, exception.ErrorCode);
|
||||
Assert.Equal(McpErrorCode.InvalidParams, exception.ErrorCode);
|
||||
|
||||
// When all data is non-serializable, the Data collection should be empty
|
||||
// (the server's ConvertExceptionData returns null when no serializable data exists)
|
||||
|
|
|
|||
|
|
@ -1033,7 +1033,7 @@ public class McpServerTests : LoggedTest
|
|||
public async Task Can_Handle_Call_Tool_Requests_With_McpProtocolException_And_Data()
|
||||
{
|
||||
const string ErrorMessage = "Resource not found";
|
||||
const McpErrorCode ErrorCode = McpErrorCode.ResourceNotFound;
|
||||
const McpErrorCode ErrorCode = McpErrorCode.InvalidParams;
|
||||
const string ResourceUri = "file:///path/to/resource";
|
||||
|
||||
await using var transport = new TestServerTransport();
|
||||
|
|
|
|||
Loading…
Reference in a new issue