Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/mcp-conformance.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ nothing a client can *observe* may depend on the connection.
| `cacheScope` / `ttlMs` on list results | Absent — not in the schema | Set to private, zero TTL | `Given_McpIntegration.When_ClientPinsLegacyProtocolVersion_Then_ListResultsCarryNoCacheHints` / `Given_McpConcurrentSessions.When_ModernClientListsTools_Then_ListResultIsTaggedPrivateAndStale` |
| Message notifications for a request that declared no log level | Emitted, subject to the session threshold | **Not emitted** — the feedback rides in the result instead | `Given_McpUserFeedback.When_RequestDeclaresNoLogLevel_Then_FeedbackRidesInTheToolResultInstead` |
| Same, through `prompts/get` | Emitted | Rides in the prompt result after the payload, and in the surfaced error when the prompt fails | `Given_McpUserFeedback.When_APromptDeclaresNoLogLevel_Then_FeedbackRidesInThePromptResultInstead` / `...When_AFailingPromptDeclaresNoLogLevel_Then_FeedbackRidesInTheError` |
| Error code for an unknown resource URI | `ResourceNotFound` (-32002) | `InvalidParams` (-32602) | `Given_McpProtocolErrorCodes.When_ReadingUnknownResource_Then_ResourceNotFoundCodeIsReturned` / `...When_ReadingUnknownResourceOnTheSessionlessRevision_Then_InvalidParamsCodeIsReturned` |

## Tool list invariance on `2026-07-28`

Expand Down Expand Up @@ -136,6 +137,7 @@ returned by a creation tool and passed back as an argument, rather than implicit

| Identifier | Status in Repl |
| --- | --- |
| SEP-2164 — standard error codes for an unknown resource or prompt | The codes the SDK's own handlers use: `InvalidParams` for an unknown prompt name on every revision, and for an unknown resource URI as the table above shows by revision |
| SEP-2549 — `cacheScope` / `ttlMs` | Set on list and resource results, on `2026-07-28` only |
| SEP-2575 — stateless requests: per-request `_meta`, and no message notification without a declared log level | The protocol version in `_meta` is what selects the era on every request; the log-level rule is honoured, and the feedback is appended to the result instead |
| SEP-2577 — Roots, Sampling and Logging deprecated | Still supported for the compatibility path; the SDK reports them under diagnostic `MCP9005` |
Expand Down
11 changes: 9 additions & 2 deletions src/Repl.Mcp/McpServerHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -408,7 +408,13 @@ private async ValueTask<ReadResourceResult> ReadResourceAsync(
var resource = snapshot.Resources.FirstOrDefault(candidate => candidate.IsMatch(uri));
if (resource is null)
{
throw new McpException($"Unknown resource: {uri}");
// The code the SDK's own resource handler uses: 2026-07-28 reports an unresolvable URI with the
// standard InvalidParams, earlier revisions with the legacy ResourceNotFound. A code-less
// McpException would surface as InternalError, which reads as a broken server, not a missing
// resource (SEP-2164).
throw new McpProtocolException(
$"Unknown resource: {uri}",
IsSessionlessRequest() ? McpErrorCode.InvalidParams : McpErrorCode.ResourceNotFound);
}

return await resource.ReadAsync(request, cancellationToken).ConfigureAwait(false);
Expand Down Expand Up @@ -439,7 +445,8 @@ private async ValueTask<GetPromptResult> GetPromptAsync(
string.Equals(candidate.ProtocolPrompt.Name, promptName, StringComparison.OrdinalIgnoreCase));
if (prompt is null)
{
throw new McpException($"Unknown prompt: {promptName}");
// InvalidParams on every revision, as the SDK's own prompt handler answers an unknown name.
throw new McpProtocolException($"Unknown prompt: {promptName}", McpErrorCode.InvalidParams);
}

return await prompt.GetAsync(request, cancellationToken).ConfigureAwait(false);
Expand Down
63 changes: 63 additions & 0 deletions src/Repl.McpTests/Given_McpProtocolErrorCodes.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
using ModelContextProtocol;
using ModelContextProtocol.Client;
using Repl.Mcp;

namespace Repl.McpTests;

/// <summary>
/// An unknown resource or prompt is a protocol error with a standard code, not an internal error:
/// hosts branch on the code to tell "gone" from "broken" (SEP-2164). The codes follow the SDK's own
/// handlers, so a Repl server answers exactly as a server built on the SDK alone would.
/// </summary>
[TestClass]
public sealed class Given_McpProtocolErrorCodes
{
[TestMethod]
[Description("An initialize-era client reading an unknown resource URI gets the legacy ResourceNotFound code (-32002), which is what the SDK's own resource handler returns on revisions before 2026-07-28. It used to get a code-less McpException, surfaced as InternalError (-32603).")]
public async Task When_ReadingUnknownResource_Then_ResourceNotFoundCodeIsReturned()
{
var error = await ReadUnknownResourceAsync(McpProtocolRevisions.LastWithSessions).ConfigureAwait(false);

error.ErrorCode.Should().Be(McpErrorCode.ResourceNotFound);
}

[TestMethod]
[Description("On 2026-07-28 an unresolvable resource URI is reported with the standard JSON-RPC InvalidParams (-32602), not the legacy ResourceNotFound — the SDK selects between the two by negotiated revision, and hard-coding the legacy code would answer a modern client with a code its revision no longer uses.")]
public async Task When_ReadingUnknownResourceOnTheSessionlessRevision_Then_InvalidParamsCodeIsReturned()
{
var error = await ReadUnknownResourceAsync(McpProtocolRevisions.Sessionless).ConfigureAwait(false);

error.ErrorCode.Should().Be(McpErrorCode.InvalidParams);
}

[TestMethod]
[DataRow(McpProtocolRevisions.LastWithSessions)]
[DataRow(McpProtocolRevisions.Sessionless)]
[Description("prompts/get with an unknown name is InvalidParams (-32602) on every revision, as the SDK's own prompt handler answers it. It used to surface as InternalError (-32603).")]
public async Task When_GettingUnknownPrompt_Then_InvalidParamsCodeIsReturned(string protocolVersion)
{
await using var fixture = await CreateFixtureAsync(protocolVersion).ConfigureAwait(false);

var act = async () => await fixture.Client.GetPromptAsync("missing").ConfigureAwait(false);

var error = await act.Should().ThrowAsync<McpProtocolException>().ConfigureAwait(false);
error.Which.ErrorCode.Should().Be(McpErrorCode.InvalidParams);
}

private static async Task<McpProtocolException> ReadUnknownResourceAsync(string protocolVersion)
{
var fixture = await CreateFixtureAsync(protocolVersion).ConfigureAwait(false);
await using (fixture.ConfigureAwait(false))
{
var act = async () => await fixture.Client.ReadResourceAsync("repl://missing").ConfigureAwait(false);

return (await act.Should().ThrowAsync<McpProtocolException>().ConfigureAwait(false)).Which;
}
}

private static Task<McpTestFixture> CreateFixtureAsync(string protocolVersion) =>
McpTestFixture.CreateAsync(
app => app.Map("status", () => "ok").ReadOnly(),
configureOptions: null,
clientOptions: new McpClientOptions { ProtocolVersion = protocolVersion });
}
Loading