diff --git a/src/Common/McpHttpHeaders.cs b/src/Common/McpHttpHeaders.cs
index 2e5ad2841..5c631a0e7 100644
--- a/src/Common/McpHttpHeaders.cs
+++ b/src/Common/McpHttpHeaders.cs
@@ -75,4 +75,12 @@ internal static class McpHttpHeaders
///
public static bool SupportsStandardHeaders(string? protocolVersion)
=> !string.IsNullOrEmpty(protocolVersion) && s_versionsWithStandardHeaders.Contains(protocolVersion!);
+
+ ///
+ /// Returns if the negotiated protocol version reports unresolvable
+ /// resource URIs with the standard JSON-RPC (-32602)
+ /// rather than the legacy (-32002).
+ ///
+ internal static bool UseInvalidParamsForMissingResource(string? protocolVersion)
+ => string.Equals(protocolVersion, MinVersionForStandardHeaders, StringComparison.Ordinal);
}
diff --git a/src/ModelContextProtocol.Core/McpErrorCode.cs b/src/ModelContextProtocol.Core/McpErrorCode.cs
index 38c5f1161..54b9eeebf 100644
--- a/src/ModelContextProtocol.Core/McpErrorCode.cs
+++ b/src/ModelContextProtocol.Core/McpErrorCode.cs
@@ -29,8 +29,17 @@ public enum McpErrorCode
/// Indicates that the requested resource could not be found.
///
///
- /// 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.
+ ///
+ /// Legacy error code for unresolvable resource URIs. Newer protocol versions report this
+ /// condition with the standard JSON-RPC (-32602) instead. The SDK
+ /// selects between the two automatically based on the negotiated protocol version, so older
+ /// clients still see (-32002) and newer ones see
+ /// .
+ ///
+ ///
+ /// New user code throwing directly for unknown-resource conditions
+ /// should prefer ; the SDK will pass the value through unchanged.
+ ///
///
ResourceNotFound = -32002,
@@ -85,6 +94,7 @@ public enum McpErrorCode
///
/// - Tools: Unknown tool name or invalid protocol-level tool arguments.
/// - Prompts: Unknown prompt name or missing required protocol-level arguments.
+ /// - Resources: Unknown or unresolvable resource URI.
/// - Pagination: Invalid or expired cursor values.
/// - Logging: Invalid log level.
/// - Tasks: Invalid or nonexistent task ID or invalid cursor.
diff --git a/src/ModelContextProtocol.Core/McpProtocolException.cs b/src/ModelContextProtocol.Core/McpProtocolException.cs
index 3fbef91c0..7bcc4d0a8 100644
--- a/src/ModelContextProtocol.Core/McpProtocolException.cs
+++ b/src/ModelContextProtocol.Core/McpProtocolException.cs
@@ -76,7 +76,7 @@ public McpProtocolException(string message, Exception? innerException, McpErrorC
/// - -32700: Parse error - Invalid JSON received
/// - -32600: Invalid request - The JSON is not a valid Request object
/// - -32601: Method not found - The method does not exist or is not available
- /// - -32602: Invalid params - Malformed request or unknown primitive name (tool/prompt/resource)
+ /// - -32602: Invalid params - Malformed request, unknown primitive name (tool/prompt/resource), or unresolvable resource URI
/// - -32603: Internal error - Internal JSON-RPC error
///
///
diff --git a/src/ModelContextProtocol.Core/Server/McpServerImpl.cs b/src/ModelContextProtocol.Core/Server/McpServerImpl.cs
index 04d11e016..203856814 100644
--- a/src/ModelContextProtocol.Core/Server/McpServerImpl.cs
+++ b/src/ModelContextProtocol.Core/Server/McpServerImpl.cs
@@ -421,7 +421,13 @@ subscribeHandler is null && unsubscribeHandler is null && resources is null &&
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;
diff --git a/tests/ModelContextProtocol.TestServer/Program.cs b/tests/ModelContextProtocol.TestServer/Program.cs
index 9cb963a96..aaa6104cc 100644
--- a/tests/ModelContextProtocol.TestServer/Program.cs
+++ b/tests/ModelContextProtocol.TestServer/Program.cs
@@ -503,7 +503,7 @@ private static void ConfigureResources(McpServerOptions options)
}
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
{
diff --git a/tests/ModelContextProtocol.TestSseServer/Program.cs b/tests/ModelContextProtocol.TestSseServer/Program.cs
index a36a0a6e0..e52a8ff1f 100644
--- a/tests/ModelContextProtocol.TestSseServer/Program.cs
+++ b/tests/ModelContextProtocol.TestSseServer/Program.cs
@@ -307,7 +307,7 @@ static CreateMessageRequestParams CreateRequestSamplingParams(string context, st
}
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
{
diff --git a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsResourcesTests.cs b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsResourcesTests.cs
index 4b03cadb2..d6bb239d7 100644
--- a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsResourcesTests.cs
+++ b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsResourcesTests.cs
@@ -109,7 +109,7 @@ protected override void ConfigureServices(ServiceCollection services, IMcpServer
};
}
- throw new McpProtocolException($"Resource not found: {request.Params.Uri}", McpErrorCode.ResourceNotFound);
+ throw new McpProtocolException($"Resource not found: {request.Params.Uri}", McpErrorCode.InvalidParams);
})
.WithResources();
}
@@ -317,7 +317,7 @@ public async Task Throws_Exception_On_Unknown_Resource()
cancellationToken: TestContext.Current.CancellationToken));
Assert.Contains("Resource not found", e.Message);
- Assert.Equal(McpErrorCode.ResourceNotFound, e.ErrorCode);
+ Assert.Equal(McpErrorCode.InvalidParams, e.ErrorCode);
}
[Fact]
diff --git a/tests/ModelContextProtocol.Tests/Configuration/McpServerResourceRoutingTests.cs b/tests/ModelContextProtocol.Tests/Configuration/McpServerResourceRoutingTests.cs
index 19e0f1bbe..1956887ac 100644
--- a/tests/ModelContextProtocol.Tests/Configuration/McpServerResourceRoutingTests.cs
+++ b/tests/ModelContextProtocol.Tests/Configuration/McpServerResourceRoutingTests.cs
@@ -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 @@ private async Task CreateClientWithResourcesAsync(params McpServerRes
return await CreateMcpClientForServer();
}
+ ///
+ /// Starts the server with the specified resources, pins both the server's and the
+ /// client's protocol version to , and returns a
+ /// connected client. Both ends must be pinned because strictly
+ /// compares the server's negotiated version against the client's requested version and
+ /// refuses to connect on mismatch.
+ ///
+ private async Task CreateClientWithResourcesAndServerVersionAsync(
+ string protocolVersion,
+ params McpServerResource[] resources)
+ {
+ McpServerBuilder.WithResources(resources);
+ McpServerBuilder.Services.Configure(o => o.ProtocolVersion = protocolVersion);
+ StartServer();
+ return await CreateMcpClientForServer(new McpClientOptions { ProtocolVersion = protocolVersion });
+ }
+
///
/// Asserts that the given URI matches the template and produces the expected text result.
///
@@ -56,6 +74,26 @@ private async Task AssertNoMatchAsync(
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(async () =>
+ await client.ReadResourceAsync("test://unknown", null, TestContext.Current.CancellationToken));
+
+ Assert.Equal(expectedCode, ex.ErrorCode);
+ }
+
///
/// 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.
diff --git a/tests/ModelContextProtocol.Tests/McpProtocolExceptionDataTests.cs b/tests/ModelContextProtocol.Tests/McpProtocolExceptionDataTests.cs
index 7d50a3044..336f58382 100644
--- a/tests/ModelContextProtocol.Tests/McpProtocolExceptionDataTests.cs
+++ b/tests/ModelContextProtocol.Tests/McpProtocolExceptionDataTests.cs
@@ -33,7 +33,7 @@ protected override void ConfigureServices(ServiceCollection services, IMcpServer
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 @@ protected override void ConfigureServices(ServiceCollection services, IMcpServer
};
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 @@ protected override void ConfigureServices(ServiceCollection services, IMcpServer
};
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 async Task Exception_With_Serializable_Data_Propagates_To_Client()
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 async Task Exception_With_NonSerializable_Data_Still_Propagates_Error_To_
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 async Task Exception_With_Only_NonSerializable_Data_Still_Propagates_Erro
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)
diff --git a/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs b/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs
index d9febd721..b8bd57b02 100644
--- a/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs
+++ b/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs
@@ -1033,7 +1033,7 @@ await transport.SendMessageAsync(
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();