Skip to content

Commit b99d887

Browse files
Deprecate McpErrorCode.ResourceNotFound (-32002) and use McpErrorCode.InvalidParams (-32602) per SEP-2164 (#1558)
1 parent 712a06b commit b99d887

10 files changed

Lines changed: 77 additions & 15 deletions

File tree

src/Common/McpHttpHeaders.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,4 +75,12 @@ internal static class McpHttpHeaders
7575
/// </summary>
7676
public static bool SupportsStandardHeaders(string? protocolVersion)
7777
=> !string.IsNullOrEmpty(protocolVersion) && s_versionsWithStandardHeaders.Contains(protocolVersion!);
78+
79+
/// <summary>
80+
/// Returns <see langword="true"/> if the negotiated protocol version reports unresolvable
81+
/// resource URIs with the standard JSON-RPC <see cref="McpErrorCode.InvalidParams"/> (-32602)
82+
/// rather than the legacy <see cref="McpErrorCode.ResourceNotFound"/> (-32002).
83+
/// </summary>
84+
internal static bool UseInvalidParamsForMissingResource(string? protocolVersion)
85+
=> string.Equals(protocolVersion, MinVersionForStandardHeaders, StringComparison.Ordinal);
7886
}

src/ModelContextProtocol.Core/McpErrorCode.cs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,17 @@ public enum McpErrorCode
2929
/// Indicates that the requested resource could not be found.
3030
/// </summary>
3131
/// <remarks>
32-
/// This error should be used when a resource URI does not match any available resource on the server.
33-
/// It allows clients to distinguish between missing resources and other types of errors.
32+
/// <para>
33+
/// Legacy error code for unresolvable resource URIs. Newer protocol versions report this
34+
/// condition with the standard JSON-RPC <see cref="InvalidParams"/> (-32602) instead. The SDK
35+
/// selects between the two automatically based on the negotiated protocol version, so older
36+
/// clients still see <see cref="ResourceNotFound"/> (-32002) and newer ones see
37+
/// <see cref="InvalidParams"/>.
38+
/// </para>
39+
/// <para>
40+
/// New user code throwing <see cref="McpProtocolException"/> directly for unknown-resource conditions
41+
/// should prefer <see cref="InvalidParams"/>; the SDK will pass the value through unchanged.
42+
/// </para>
3443
/// </remarks>
3544
ResourceNotFound = -32002,
3645

@@ -85,6 +94,7 @@ public enum McpErrorCode
8594
/// <list type="bullet">
8695
/// <item><description><b>Tools</b>: Unknown tool name or invalid protocol-level tool arguments.</description></item>
8796
/// <item><description><b>Prompts</b>: Unknown prompt name or missing required protocol-level arguments.</description></item>
97+
/// <item><description><b>Resources</b>: Unknown or unresolvable resource URI.</description></item>
8898
/// <item><description><b>Pagination</b>: Invalid or expired cursor values.</description></item>
8999
/// <item><description><b>Logging</b>: Invalid log level.</description></item>
90100
/// <item><description><b>Tasks</b>: Invalid or nonexistent task ID or invalid cursor.</description></item>

src/ModelContextProtocol.Core/McpProtocolException.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ public McpProtocolException(string message, Exception? innerException, McpErrorC
7676
/// <item><description>-32700: Parse error - Invalid JSON received</description></item>
7777
/// <item><description>-32600: Invalid request - The JSON is not a valid Request object</description></item>
7878
/// <item><description>-32601: Method not found - The method does not exist or is not available</description></item>
79-
/// <item><description>-32602: Invalid params - Malformed request or unknown primitive name (tool/prompt/resource)</description></item>
79+
/// <item><description>-32602: Invalid params - Malformed request, unknown primitive name (tool/prompt/resource), or unresolvable resource URI</description></item>
8080
/// <item><description>-32603: Internal error - Internal JSON-RPC error</description></item>
8181
/// </list>
8282
/// </remarks>

src/ModelContextProtocol.Core/Server/McpServerImpl.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -421,7 +421,13 @@ subscribeHandler is null && unsubscribeHandler is null && resources is null &&
421421

422422
listResourcesHandler ??= (static async (_, __) => new ListResourcesResult());
423423
listResourceTemplatesHandler ??= (static async (_, __) => new ListResourceTemplatesResult());
424-
readResourceHandler ??= (static async (request, _) => throw new McpProtocolException($"Unknown resource URI: '{request.Params?.Uri}'", McpErrorCode.ResourceNotFound));
424+
readResourceHandler ??= (static async (request, _) =>
425+
{
426+
var errorCode = McpHttpHeaders.UseInvalidParamsForMissingResource(request.Server.NegotiatedProtocolVersion)
427+
? McpErrorCode.InvalidParams
428+
: McpErrorCode.ResourceNotFound;
429+
throw new McpProtocolException($"Unknown resource URI: '{request.Params?.Uri}'", errorCode);
430+
});
425431
subscribeHandler ??= (static async (_, __) => new EmptyResult());
426432
unsubscribeHandler ??= (static async (_, __) => new EmptyResult());
427433
var listChanged = resourcesCapability?.ListChanged;

tests/ModelContextProtocol.TestServer/Program.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -503,7 +503,7 @@ private static void ConfigureResources(McpServerOptions options)
503503
}
504504

505505
ResourceContents contents = resourceContents.FirstOrDefault(r => r.Uri == request.Params.Uri)
506-
?? throw new McpProtocolException($"Resource not found: '{request.Params.Uri}'", McpErrorCode.ResourceNotFound);
506+
?? throw new McpProtocolException($"Resource not found: '{request.Params.Uri}'", McpErrorCode.InvalidParams);
507507

508508
return new ReadResourceResult
509509
{

tests/ModelContextProtocol.TestSseServer/Program.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,7 @@ static CreateMessageRequestParams CreateRequestSamplingParams(string context, st
307307
}
308308

309309
ResourceContents? contents = resourceContents.FirstOrDefault(r => r.Uri == request.Params.Uri) ??
310-
throw new McpProtocolException($"Resource not found: '{request.Params.Uri}'", McpErrorCode.ResourceNotFound);
310+
throw new McpProtocolException($"Resource not found: '{request.Params.Uri}'", McpErrorCode.InvalidParams);
311311

312312
return new ReadResourceResult
313313
{

tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsResourcesTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ protected override void ConfigureServices(ServiceCollection services, IMcpServer
109109
};
110110
}
111111

112-
throw new McpProtocolException($"Resource not found: {request.Params.Uri}", McpErrorCode.ResourceNotFound);
112+
throw new McpProtocolException($"Resource not found: {request.Params.Uri}", McpErrorCode.InvalidParams);
113113
})
114114
.WithResources<SimpleResources>();
115115
}
@@ -317,7 +317,7 @@ public async Task Throws_Exception_On_Unknown_Resource()
317317
cancellationToken: TestContext.Current.CancellationToken));
318318

319319
Assert.Contains("Resource not found", e.Message);
320-
Assert.Equal(McpErrorCode.ResourceNotFound, e.ErrorCode);
320+
Assert.Equal(McpErrorCode.InvalidParams, e.ErrorCode);
321321
}
322322

323323
[Fact]

tests/ModelContextProtocol.Tests/Configuration/McpServerResourceRoutingTests.cs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using Microsoft.Extensions.DependencyInjection;
2+
using Microsoft.Extensions.Options;
23
using ModelContextProtocol.Client;
34
using ModelContextProtocol.Protocol;
45
using ModelContextProtocol.Server;
@@ -22,6 +23,23 @@ private async Task<McpClient> CreateClientWithResourcesAsync(params McpServerRes
2223
return await CreateMcpClientForServer();
2324
}
2425

26+
/// <summary>
27+
/// Starts the server with the specified resources, pins both the server's and the
28+
/// client's protocol version to <paramref name="protocolVersion"/>, and returns a
29+
/// connected client. Both ends must be pinned because <see cref="McpClient"/> strictly
30+
/// compares the server's negotiated version against the client's requested version and
31+
/// refuses to connect on mismatch.
32+
/// </summary>
33+
private async Task<McpClient> CreateClientWithResourcesAndServerVersionAsync(
34+
string protocolVersion,
35+
params McpServerResource[] resources)
36+
{
37+
McpServerBuilder.WithResources(resources);
38+
McpServerBuilder.Services.Configure<McpServerOptions>(o => o.ProtocolVersion = protocolVersion);
39+
StartServer();
40+
return await CreateMcpClientForServer(new McpClientOptions { ProtocolVersion = protocolVersion });
41+
}
42+
2543
/// <summary>
2644
/// Asserts that the given URI matches the template and produces the expected text result.
2745
/// </summary>
@@ -56,6 +74,26 @@ private async Task AssertNoMatchAsync(
5674
Assert.Equal(McpErrorCode.ResourceNotFound, ex.ErrorCode);
5775
}
5876

77+
// Unknown-resource-URI responses are version-gated: older clients keep the legacy
78+
// -32002 (McpErrorCode.ResourceNotFound), and clients on the draft protocol version that
79+
// moves to the standard JSON-RPC code see -32602 (McpErrorCode.InvalidParams).
80+
[Theory]
81+
[InlineData("2025-11-25", McpErrorCode.ResourceNotFound)]
82+
[InlineData("DRAFT-2026-v1", McpErrorCode.InvalidParams)]
83+
public async Task ResourceNotFound_ErrorCode_IsVersionGated(string serverProtocolVersion, McpErrorCode expectedCode)
84+
{
85+
var resource = McpServerResource.Create(
86+
options: new() { UriTemplate = "test://known/{id}" },
87+
method: (string id) => $"ok: {id}");
88+
89+
var client = await CreateClientWithResourcesAndServerVersionAsync(serverProtocolVersion, resource);
90+
91+
var ex = await Assert.ThrowsAsync<McpProtocolException>(async () =>
92+
await client.ReadResourceAsync("test://unknown", null, TestContext.Current.CancellationToken));
93+
94+
Assert.Equal(expectedCode, ex.ErrorCode);
95+
}
96+
5997
/// <summary>
6098
/// Verify that when multiple templated resources exist, the correct one is matched based on the URI pattern.
6199
/// Regression test for https://github.com/modelcontextprotocol/csharp-sdk/issues/821.

tests/ModelContextProtocol.Tests/McpProtocolExceptionDataTests.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ protected override void ConfigureServices(ServiceCollection services, IMcpServer
3333
switch (toolName)
3434
{
3535
case "throw_with_serializable_data":
36-
throw new McpProtocolException("Resource not found", McpErrorCode.ResourceNotFound)
36+
throw new McpProtocolException("Resource not found", McpErrorCode.InvalidParams)
3737
{
3838
Data =
3939
{
@@ -43,7 +43,7 @@ protected override void ConfigureServices(ServiceCollection services, IMcpServer
4343
};
4444

4545
case "throw_with_nonserializable_data":
46-
throw new McpProtocolException("Resource not found", McpErrorCode.ResourceNotFound)
46+
throw new McpProtocolException("Resource not found", McpErrorCode.InvalidParams)
4747
{
4848
Data =
4949
{
@@ -55,7 +55,7 @@ protected override void ConfigureServices(ServiceCollection services, IMcpServer
5555
};
5656

5757
case "throw_with_only_nonserializable_data":
58-
throw new McpProtocolException("Resource not found", McpErrorCode.ResourceNotFound)
58+
throw new McpProtocolException("Resource not found", McpErrorCode.InvalidParams)
5959
{
6060
Data =
6161
{
@@ -79,7 +79,7 @@ public async Task Exception_With_Serializable_Data_Propagates_To_Client()
7979
await client.CallToolAsync("throw_with_serializable_data", cancellationToken: TestContext.Current.CancellationToken));
8080

8181
Assert.Equal("Request failed (remote): Resource not found", exception.Message);
82-
Assert.Equal(McpErrorCode.ResourceNotFound, exception.ErrorCode);
82+
Assert.Equal(McpErrorCode.InvalidParams, exception.ErrorCode);
8383

8484
// Verify the data was propagated to the exception
8585
// The Data collection should contain the expected keys
@@ -113,7 +113,7 @@ public async Task Exception_With_NonSerializable_Data_Still_Propagates_Error_To_
113113
await client.CallToolAsync("throw_with_nonserializable_data", cancellationToken: TestContext.Current.CancellationToken));
114114

115115
Assert.Equal("Request failed (remote): Resource not found", exception.Message);
116-
Assert.Equal(McpErrorCode.ResourceNotFound, exception.ErrorCode);
116+
Assert.Equal(McpErrorCode.InvalidParams, exception.ErrorCode);
117117

118118
// Verify that only the serializable data was propagated (non-serializable was filtered out)
119119
var hasUri = false;
@@ -142,7 +142,7 @@ public async Task Exception_With_Only_NonSerializable_Data_Still_Propagates_Erro
142142
await client.CallToolAsync("throw_with_only_nonserializable_data", cancellationToken: TestContext.Current.CancellationToken));
143143

144144
Assert.Equal("Request failed (remote): Resource not found", exception.Message);
145-
Assert.Equal(McpErrorCode.ResourceNotFound, exception.ErrorCode);
145+
Assert.Equal(McpErrorCode.InvalidParams, exception.ErrorCode);
146146

147147
// When all data is non-serializable, the Data collection should be empty
148148
// (the server's ConvertExceptionData returns null when no serializable data exists)

tests/ModelContextProtocol.Tests/Server/McpServerTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1033,7 +1033,7 @@ await transport.SendMessageAsync(
10331033
public async Task Can_Handle_Call_Tool_Requests_With_McpProtocolException_And_Data()
10341034
{
10351035
const string ErrorMessage = "Resource not found";
1036-
const McpErrorCode ErrorCode = McpErrorCode.ResourceNotFound;
1036+
const McpErrorCode ErrorCode = McpErrorCode.InvalidParams;
10371037
const string ResourceUri = "file:///path/to/resource";
10381038

10391039
await using var transport = new TestServerTransport();

0 commit comments

Comments
 (0)