Skip to content

mcp: add MCP 2026-07-28 protocol support - #2878

Open
HarshPopat23 wants to merge 3 commits into
sourcemeta:mainfrom
HarshPopat23:feat/mcp-2026-07-28-version
Open

HarshPopat23 wants to merge 3 commits into
sourcemeta:mainfrom
HarshPopat23:feat/mcp-2026-07-28-version

Conversation

@HarshPopat23

@HarshPopat23 HarshPopat23 commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Expand PR #2878 to add MCP 2026-07-28 protocol support. As requested, this description outlines the Required Core Parts (mandatory for MCP 2026-07-28 compliance) vs Optional / Extended Features and provides an explicit progress checklist to make review easier.


📋 Progress & Implementation Status

🟢 Required Core Protocol Foundation (Completed in this PR)

  • Protocol Version & Feature Gates: MCPProtocolVersion::V_2026_07_28 ("2026-07-28") enum, string parsing, serialization, and chronological ordering.
  • Handshake-free Lifecycle: mcp_uses_initialization_handshake() returns false for 2026-07-28; legacy initialize negotiation correctly falls back to 2025-11-25 via mcp_latest_initialization_version().
  • Mandatory Stateless Request Metadata (_meta): Strict validation via mcp_validate_request_meta:
    • Protocol version check (io.modelcontextprotocol/protocolVersion).
    • Client capabilities object check (io.modelcontextprotocol/clientCapabilities).
    • Client implementation info check (io.modelcontextprotocol/clientInfo with required string name and version, plus optional title/description string type validation).
    • Safe convenience accessors (mcp_request_protocol_version, mcp_request_client_info, mcp_request_client_capabilities, mcp_has_required_request_meta).
  • Streamable HTTP Routing Headers: Version-aware validation via mcp_validate_request_headers:
    • Required MCP-Protocol-Version matching request _meta.
    • Required Mcp-Method matching JSON-RPC body method.
    • Required Mcp-Name on named requests (tools/call, resources/read, prompts/get), including RFC 9110 Base64 header decoding (=?base64?...?=).
    • Rejection of unexpected Mcp-Name on unnamed requests.
    • Backward-compatibility: routing headers remain optional for legacy protocol revisions (2025-03-26, 2025-06-18, 2025-11-25).
  • Modern Protocol Errors:
    • -32020 (Header mismatch) with structured header/body details.
    • -32021 (Missing required client capability) with requiredCapabilities payload.
    • -32022 (Unsupported protocol version) with requested/supported versions list.
    • -32602 (Invalid params): version-aware resource not found error for 2026-07-28 (preserving -32002 for legacy revisions).
  • Mandatory Discovery Endpoint (server/discover): Discovery response builder (mcp_make_server_discover_result) returning supported versions, server capabilities, and server info under _meta.
  • Result Envelopes: Modern decorator emitting resultType: "complete" (mcp_decorate_result).

🟡 Optional / Extended Protocol Primitives (Included in this PR)

  • Cache Control Primitives: Optional ttlMs and cacheScope (public/private) emitted for cacheable results (tools/list, resources/list, resources/templates/list).
  • Multi Round-Trip Requests (MRTR): Primitives for resultType: "input_required", optional inputRequests, and opaque requestState.
  • Subscription Primitives: Primitives for subscriptions/listen and notifications/subscriptions/acknowledged.
  • Capability Extensions: Client and server capability extensions and experimental maps.

⏩ Subsequent PRs & Follow-ups

  • Transport-level HTTP/SSE server socket and session management in sourcemeta/one.
  • Additional specialized transport adapters if needed.

Backward Compatibility

  • All legacy MCP protocol revisions (2025-03-26, 2025-06-18, 2025-11-25) remain byte-for-byte identical in behavior and serialized output.
  • Transports without HTTP headers (such as stdio) continue to rely purely on metadata and JSON-RPC validation without requiring HTTP routing headers.

Testing & Quality

  • Complete MCP unit test suite: 204 passing tests in test/mcp/mcp_test.cc (covering all positive, negative, and edge cases).
  • ClangFormat compliance verified (cmake --build build --target clang_format_test).
  • Git diff whitespace check verified (git diff --check).
  • CTest suite execution verified (100% pass across all platforms in CI).

Signed-off-by: HarshPopat23 <musichk61@gmail.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 2 files

Re-trigger cubic

@augmentcode

augmentcode Bot commented Sep 25, 2026

Copy link
Copy Markdown
🤖 Augment PR Summary

Summary: This PR begins support for MCP protocol revision 2026-07-28.

Changes:

  • Adds MCPProtocolVersion::V_2026_07_28 and its canonical wire string.
  • Extends exact protocol-header parsing to recognize 2026-07-28.
  • Introduces chronological comparison through mcp_protocol_version_at_least.
  • Updates feature predicates to use revision thresholds rather than equality checks.
  • Preserves feature behavior for the pre-existing protocol revisions.
  • Adds unit coverage for conversion, parsing, ordering, and feature gates.

Technical Notes: The change intentionally excludes the broader 2026-07-28 feature and transport upgrades.

🤖 Was this summary useful? React with 👍 or 👎

@augmentcode augmentcode Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review completed. 1 suggestion posted.

Fix All in Augment

Comment augment review to trigger a new review at any time.

// https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#protocol-version-header
return MCPProtocolVersion::V_2025_03_26;
}
if (header == "2026-07-28") {

@augmentcode augmentcode Bot Sep 25, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[src/core/mcp/include/sourcemeta/core/mcp.h:181] Adding this revision makes it supported, but the unsupported-version negotiation fallback still selects V_2025_11_25 in mcp_make_initialize_result (src/core/mcp/mcp.cc:342). As a result, an initialization request for an unknown version still receives 2025-11-25, not the latest supported 2026-07-28, contrary to that path's stated negotiation behavior.

Severity: medium

Other Locations
  • src/core/mcp/mcp.cc:342

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is intentional for PR 1. This PR only introduces the version enum and comparison infrastructure. The actual MCP 2026-07-28 features (such as discovery, modern result envelopes, and stateless requests) are being implemented in subsequent PRs. Falling back to 2026-07-28 before those features are implemented would cause the server to advertise a protocol version it does not yet fully support. The negotiation fallback will be bumped once the feature implementation is complete.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2025-11-25 is intentionally the newest initialization-compatible protocol revision returned by mcp_make_initialize_result. In the MCP 2026-07-28 specification, the initialization handshake (initialize /
otifications/initialized) was completely removed in favor of stateless per-request metadata (_meta) and discovery (server/discover). Negotiating 2026-07-28 in an initialize response would violate modern MCP protocol semantics for clients expecting the handshake-free lifecycle. Hence, legacy initialize requests correctly fall back to 2025-11-25 via mcp_latest_initialization_version().

@HarshPopat23

Copy link
Copy Markdown
Contributor Author

PTAL : @jviotti

@jviotti

jviotti commented Sep 26, 2026

Copy link
Copy Markdown
Member

Looks good so far, but merging this as-is, without the other changes, would make it tricky for i.e. sourcemeta/one, which relies on some of these checks.

Can we try to land support for the new version all in one shot?

@HarshPopat23

Copy link
Copy Markdown
Contributor Author

Why not @jviotti ,
I will try to solve in next commit.

@HarshPopat23 HarshPopat23 changed the title mcp: add the 2026-07-28 protocol version mcp: add MCP 2026-07-28 protocol support Sep 26, 2026

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 3 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread src/core/mcp/mcp.cc Outdated
@HarshPopat23
HarshPopat23 force-pushed the feat/mcp-2026-07-28-version branch 3 times, most recently from d07653c to c32456f Compare September 26, 2026 21:12
Signed-off-by: HarshPopat23 <musichk61@gmail.com>
@HarshPopat23
HarshPopat23 force-pushed the feat/mcp-2026-07-28-version branch from c32456f to 11c9459 Compare September 26, 2026 21:32
@HarshPopat23

Copy link
Copy Markdown
Contributor Author

@jviotti
Got it-by “one shot,” I understand that PR #2878 should contain the complete Core-level MCP 2026-07-28 support rather than only the version infrastructure. I’ll extend this same PR with stateless request metadata, modern errors, discovery, result envelopes, cache hints, MRTR and subscription primitives, while preserving exact legacy behaviour. Transport integration in Sourcemeta One will remain a follow-up after the Core API is merged.

Signed-off-by: HarshPopat23 <musichk61@gmail.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 issues found across 3 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/core/mcp/include/sourcemeta/core/mcp.h">

<violation number="1" location="src/core/mcp/include/sourcemeta/core/mcp.h:753">
P3: `Mcp-Name` is mandatory for named methods: `mcp_validate_request_headers` rejects its absence. Remove “optionally” so HTTP callers know these requests must include it.</violation>
</file>

<file name="src/core/mcp/mcp.cc">

<violation number="1" location="src/core/mcp/mcp.cc:528">
P2: This signed accumulator overflows while decoding ordinary base64 header values, including the URI used by the existing test. Make it unsigned (or bound the retained bits) so decoding does not invoke undefined behavior.</violation>

<violation number="2" location="src/core/mcp/mcp.cc:680">
P1: A supported but different protocol header passes this legacy branch. Compare the resolved version with `version` and return a header-mismatch error when they differ; otherwise a legacy request can declare `2026-07-28` while bypassing modern header requirements.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread src/core/mcp/mcp.cc
Comment on lines +680 to +684
if (!resolved_protocol.has_value()) {
return mcp_make_error_unsupported_protocol_version(
request_id, protocol_version_header.value(),
mcp_supported_protocol_versions());
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: A supported but different protocol header passes this legacy branch. Compare the resolved version with version and return a header-mismatch error when they differ; otherwise a legacy request can declare 2026-07-28 while bypassing modern header requirements.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/core/mcp/mcp.cc, line 680:

<comment>A supported but different protocol header passes this legacy branch. Compare the resolved version with `version` and return a header-mismatch error when they differ; otherwise a legacy request can declare `2026-07-28` while bypassing modern header requirements.</comment>

<file context>
@@ -523,28 +614,162 @@ auto mcp_make_error_header_mismatch(const sourcemeta::core::JSON *identifier,
+
+    const auto resolved_protocol{
+        mcp_resolve_protocol_version(protocol_version_header.value())};
+    if (!resolved_protocol.has_value()) {
+      return mcp_make_error_unsupported_protocol_version(
+          request_id, protocol_version_header.value(),
</file context>
Suggested change
if (!resolved_protocol.has_value()) {
return mcp_make_error_unsupported_protocol_version(
request_id, protocol_version_header.value(),
mcp_supported_protocol_versions());
}
if (!resolved_protocol.has_value()) {
return mcp_make_error_unsupported_protocol_version(
request_id, protocol_version_header.value(),
mcp_supported_protocol_versions());
}
if (resolved_protocol.value() != version) {
return mcp_make_error_header_mismatch(
request_id, MCP_HEADER_PROTOCOL_VERSION,
protocol_version_header.value(), mcp_protocol_version_string(version));
}

Comment thread src/core/mcp/mcp.cc
-> std::optional<std::string> {
std::string decoded;
decoded.reserve(encoded.size() * 3 / 4);
int accumulator = 0;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This signed accumulator overflows while decoding ordinary base64 header values, including the URI used by the existing test. Make it unsigned (or bound the retained bits) so decoding does not invoke undefined behavior.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/core/mcp/mcp.cc, line 528:

<comment>This signed accumulator overflows while decoding ordinary base64 header values, including the URI used by the existing test. Make it unsigned (or bound the retained bits) so decoding does not invoke undefined behavior.</comment>

<file context>
@@ -467,6 +500,64 @@ auto mcp_request_name_from_body(const sourcemeta::core::JSON &envelope)
+    -> std::optional<std::string> {
+  std::string decoded;
+  decoded.reserve(encoded.size() * 3 / 4);
+  int accumulator = 0;
+  int bit_count = -8;
+  for (const char character : encoded) {
</file context>
Suggested change
int accumulator = 0;
unsigned int accumulator = 0;

/// assert(block.at("text").to_string() == "hello");
/// ```
/// For MCP 2026-07-28, standard routing headers (`MCP-Protocol-Version`,
/// `Mcp-Method`, and optionally `Mcp-Name` for named methods) are required.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Mcp-Name is mandatory for named methods: mcp_validate_request_headers rejects its absence. Remove “optionally” so HTTP callers know these requests must include it.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/core/mcp/include/sourcemeta/core/mcp.h, line 753:

<comment>`Mcp-Name` is mandatory for named methods: `mcp_validate_request_headers` rejects its absence. Remove “optionally” so HTTP callers know these requests must include it.</comment>

<file context>
@@ -702,13 +733,36 @@ auto mcp_make_error_header_mismatch(const sourcemeta::core::JSON *identifier,
+/// helper.
+///
+/// For MCP 2026-07-28, standard routing headers (`MCP-Protocol-Version`,
+/// `Mcp-Method`, and optionally `Mcp-Name` for named methods) are required.
+/// For legacy revisions, routing headers are optional but verified if
+/// present.
</file context>
Suggested change
/// `Mcp-Method`, and optionally `Mcp-Name` for named methods) are required.
/// `Mcp-Method`, and `Mcp-Name` for named methods) are required.

@HarshPopat23

Copy link
Copy Markdown
Contributor Author

@jviotti ,
In commit b5487a8, I resolved the remaining MCP 2026-07-28 compliance gaps:

  • Required clientInfo Validation: io.modelcontextprotocol/clientInfo is now mandatory in _meta. Validates object type and string name/version (plus optional title/description), rejecting empty{}or malformed fields with explicit status codes.

  • Streamable HTTP Header Enforcement: Updated mcp_validate_request_headers to be version-aware. For 2026-07-28, it enforces required MCP-Protocol-Version, Mcp-Method, and Mcp-Name (on named methods, supporting RFC 9110 Base64) with -32020 / -32022 errors on missing/mismatched headers, while preserving optional routing headers for legacy versions.

@jviotti

jviotti commented Sep 28, 2026

Copy link
Copy Markdown
Member

here is what my own AI round on this revealed, by trying to integrate this on Sourcemeta One. Please triple check! I'll do a more careful review later (just a very hectic day so far):


1. Spec violations

1.1 clientInfo is treated as required, but it is optional

mcp.cc:357 returns MissingClientInfo with std::nullopt when the key is
absent.

spec:basic/index#meta, under the bold label "Per-request protocol fields"
(Description column elided):

Key Type Required
io.modelcontextprotocol/protocolVersion string Yes
io.modelcontextprotocol/clientInfo Implementation No
io.modelcontextprotocol/clientCapabilities ClientCapabilities Yes
io.modelcontextprotocol/logLevel LoggingLevel No

Same section: "Clients SHOULD include
io.modelcontextprotocol/clientInfo on every request unless specifically
configured not to do so."

schema.ts, RequestMetaObject, confirms the optional marker (non-adjacent
lines, intervening doc comments elided):

"io.modelcontextprotocol/protocolVersion": string;
// ...
"io.modelcontextprotocol/clientInfo"?: Implementation;
// ...
"io.modelcontextprotocol/clientCapabilities": ClientCapabilities;

Two consequences. A conformant client is rejected. And because
mcp_request_protocol_version (mcp.cc:415),
mcp_request_client_capabilities (mcp.cc:433) and mcp_request_log_level
(mcp.cc:442) all return early on an empty meta optional, such a request
also loses its protocol version, capabilities and log level.

MCPRequestMeta::client_info (mcp.h:643) is already std::optional, so
optionality was intended. test/mcp/mcp_test.cc:1820 asserts the wrong
behavior and must change with the fix.

When the key is present, name and version stay required, so that half of
the validation is correct and should be kept:

schema.ts, RequestMetaObject: "The {@link Implementation} schema requires
name and version; other fields are optional."

1.2 subscriptionId is typed as a string, but it is string | number

mcp.h:1049, mcp.h:1056, mcp.h:1064, and mcp.cc:1434 gates on
is_string().

schema.ts, NotificationMetaObject and RequestId:

"io.modelcontextprotocol/subscriptionId"?: RequestId;
export type RequestId = string | number;

spec:basic/patterns/subscriptions#receiving-notifications: "The value is
the JSON-RPC ID of the subscriptions/listen request."

The examples on that page use "id": 1 and therefore
"io.modelcontextprotocol/subscriptionId": 1. So the builders cannot emit a
spec-shaped acknowledgment, and the accessor returns std::nullopt on
conformant input, silently dropping stream correlation. The tests only exercise
string ids ("sub-42", "stream-99"), which is why this passes.

Separately, mcp_make_subscription_close_result (mcp.h:1056) takes the
identifier and the subscription id as two arguments when the spec defines them
as one value. Derive it from the identifier.

spec:basic/patterns/subscriptions#graceful-closure: "The value matches the
JSON-RPC id of the originating subscriptions/listen request."

schema.ts, SubscriptionsListenResultMetaObject: "(and equals this
response's id)".

1.3 Cache metadata is optional by default, but it is required

spec:server/utilities/caching#cacheable-results: "Servers MUST include
caching hints on results with resultType: "complete" returned by the
following operations:", followed by a list of server/discover,
tools/list, prompts/list, resources/list,
resources/templates/list and resources/read.

Both fields are non-optional on the interface that carries them:

export interface CacheableResult extends Result {
  ttlMs: number;
  cacheScope: "public" | "private";
}

mcp_decorate_cacheable_result (mcp.cc:898) returns the result untouched
when cache_policy is std::nullopt, and four of the six builders default it
to std::nullopt: mcp_make_tools_list_result (mcp.h:943),
mcp_make_resources_list_result (mcp.h:953),
mcp_make_resource_templates_list_result (mcp.h:963) and
mcp_make_resources_read_result (mcp.h:893). server/discover already takes
a non-optional policy, and prompts/list has no builder yet.

Two constraints on the fix. The parameter cannot simply become mandatory for
all revisions, because these builders are version-parameterised and
mcp_requires_cacheable_metadata is false for the three older ones, so
pre-2026 callers would be forced to supply a value that is then discarded. And
a = {} default is not a remedy either, because MCPCachePolicy defaults to
MCPCacheScope::Public:

spec:server/utilities/caching#security-considerations: "Servers MUST be
aware that responses with a "public" cacheScope may be shared between
callers even if the Result is coming from an authenticated endpoint."

Splitting the 2026 builders from the legacy ones, so the policy is a required
argument only where it is emitted, avoids both.

1.4 An empty protocolVersion in _meta is accepted as 2025-03-26

mcp.cc:339-343 resolves the field through mcp_resolve_protocol_version,
whose empty-input branch returns V_2025_03_26 (mcp.h:340-342). A _meta
that is otherwise complete but declares "io.modelcontextprotocol/protocolVersion": ""
therefore validates as Valid with protocol_version == V_2025_03_26.

That fallback is defined for an omitted HTTP header, not for the _meta field:

spec:basic/transports/streamable-http#protocol-version-header: "A server
that supports clients implementing protocol versions earlier than
2025-06-18 (which did not define the MCP-Protocol-Version header)
MAY treat a request that omits the header as protocol version
2025-03-26."

An empty string is not a version the server supports, so it belongs on the
existing UnsupportedProtocolVersion status:

schema.ts, RequestMetaObject: "If the server does not support the
requested version, it MUST return an {@link UnsupportedProtocolVersionError}."

Note this is -32022, not -32602. The -32602 rule quoted in 2.2 covers a
field that is absent, not one that is present and empty.

1.5 mcp_make_input_required_result can emit a result with neither field

spec:basic/patterns/mrtr#server-requirements-basic-workflow, item 6:
"Servers MUST include at least one of inputRequests or requestState
in every InputRequiredResult response."

Both parameters default to std::nullopt (mcp.h:1030-1031) and the only
enforcement is assert(input_requests.has_value() || request_state.has_value())
(mcp.cc:1321). Under NDEBUG, mcp_make_input_required_result(identifier)
compiles and emits {"resultType": "input_required"}, violating that MUST.
Drop the defaults and take the constraint in the type, as a variant or two
separate entry points.

1.6 Nothing constrains which methods may carry an InputRequiredResult

spec:basic/patterns/mrtr#supported-requests: servers MAY send
InputRequiredResult on prompts/get, resources/read and tools/call,
then "Servers MUST NOT send InputRequiredResult responses on any other
client requests."

mcp_make_input_required_result takes neither a method nor a version, so the
module cannot express that restriction. The mismatch is stark against the method
table: of the three methods allowed to carry one, prompts/get is not a
recognised request method for 2026-07-28 (mcp.h:236-243), while every method
that is recognised (server/discover, tools/list, resources/list,
resources/templates/list, subscriptions/listen) is forbidden from carrying
one. See 2.6.


2. Missing surface

2.1 No Mcp-Param-* validation and no x-mcp-header parsing

Nothing in the PR mentions either. Case-insensitive greps for mcp-param,
x-mcp and McpParam across the header, source and tests return zero hits.

spec:basic/transports/streamable-http#server-behavior-for-custom-headers:
"Servers MUST reject requests with a recognized Mcp-Param-{Name} header
that contains invalid characters" (trailing cross-reference elided).

Same section, next paragraph: "Any server that processes the message body
MUST validate that encoded header values, after decoding if
Base64-encoded, match the corresponding values in the request body. Servers
MUST reject requests with a 400 Bad Request HTTP status and JSON-RPC
error code -32020 (HeaderMismatch) if any validation fails."

Those MUSTs are about validating headers a server receives. Emitting the
annotation is only a MAY for servers:

spec:basic/transports/streamable-http#custom-headers-from-tool-parameters:
"While the use of x-mcp-header is optional for servers, clients MUST
support this feature."

So x-mcp-header parsing is required transitively, not textually: a server
cannot validate Mcp-Param-* without knowing which parameters are annotated.
The PR description lists the routing-header work as complete and never mentions
either name.

2.2 No mapping from MCPRequestMetaStatus to an error response

spec:basic/index#meta: "A request missing any required field is malformed;
the server MUST reject it with JSON-RPC error code -32602 (Invalid
params). On HTTP, the response status MUST be 400 Bad Request."

The envelope builder already exists and is public,
jsonrpc_make_error_invalid_params, and the PR calls it at mcp.cc:1194 and
emits -32602 directly at mcp.cc:657, mcp.cc:668 and mcp.cc:784. So the
gap is not a missing builder. It is that mcp_validate_request_meta hands back
one of 18 enumerators and nothing converts that into a code and a message, so
every consumer writes the same 18-way switch. Note the rule above covers absent
required fields, which is MissingProtocolVersion and
MissingClientCapabilities, not the empty-value case in 1.4.

2.3 No error-code to HTTP-status mapping

Three MUSTs, all HTTP-binding rules a JSON-RPC-only builder cannot express.
Greps for http_status, status_code, 400 and 404 across the header and
source return zero hits.

spec:basic/index#meta: "On HTTP, the response status MUST be
400 Bad Request." (stated twice in that section, once for the
malformed-required-field rule and once for -32021)

spec:basic/transports/streamable-http#protocol-version-header: "If the
server does not implement the requested protocol version ... it MUST
respond with 400 Bad Request and an UnsupportedProtocolVersionError
listing its supported versions."

Same section: "If the server does not implement the requested RPC method, it
MUST respond with 404 Not Found and a JSON-RPC error with code
-32601 (Method not found)."

2.4 No named empty-result builder

schema.ts: export type EmptyResult = Result;, and Result.resultType is
non-optional (resultType: ResultType;).

spec:basic/index#result-responses: "The result MUST include a
resultType field to indicate the type of the result."

A convenience gap rather than a compliance one:
mcp_decorate_result(version, JSON::make_object()) already yields
{"resultType": "complete"}, because mcp.cc:851 assigns the field when
absent. But no named entry point exists, and EmptyResult, empty_result and
make_empty appear nowhere in the PR, so callers reach for the decorator by
convention rather than by API.

2.5 MCPClientCapabilities drops the sub-capabilities MRTR needs

mcp.h:562 models sampling and elicitation as bare bools.
mcp_parse_client_capabilities sets them true on mere object presence and never
reads the sub-fields, and mcp_serialize_client_capabilities emits {} for
both. schema.ts ClientCapabilities defines four of them:

sampling?: { context?: JSONObject; tools?: JSONObject; };
elicitation?: { form?: JSONObject; url?: JSONObject; };

spec:basic/patterns/mrtr#server-requirements-basic-workflow, item 7:
"Servers MUST NOT send an inputRequests that the client has not
declared support for in its capabilities."

URL-mode elicitation cannot be gated without the url sub-flag.

2.6 Method tables are incomplete and self-contradictory

mcp_classify_method (mcp.h:278) recognises eleven names, three of which
(initialize, ping, notifications/initialized) no longer exist in this
revision. Everything else falls through to Unsupported, so
mcp_supports_method(V_2026_07_28, m) wrongly returns false for exactly
thirteen methods that schema.ts does define:

completion/complete, elicitation/create, notifications/cancelled,
notifications/message, notifications/progress,
notifications/prompts/list_changed,
notifications/resources/list_changed, notifications/resources/updated,
notifications/tools/list_changed, prompts/get, prompts/list,
roots/list, sampling/createMessage.

Not every notification is missed: notifications/subscriptions/acknowledged is
classified ModernOnly (mcp.h:286) and notifications/initialized is
LegacyOnly (mcp.h:281). And three of the thirteen (roots/list,
sampling/createMessage, elicitation/create) are client-side requests that
this revision carries only inside MRTR inputRequests, so whether a server-side
request-method predicate should recognise them at all is a design question, not
a straight omission. The remaining ten are.

Directly contradictory within the PR: mcp_is_named_request_method
(mcp.h:150) returns true for prompts/get, and
mcp_validate_request_headers (mcp.cc:632) consults only that predicate, so
mcp.cc:723-727 returns -32020 "Missing required header: Mcp-Name" for a
prompts/get body while mcp_is_request_method(V_2026_07_28, "prompts/get")
(mcp.h:233) denies the method exists.

The Mcp-Name requirement itself is correct:

spec:basic/transports/streamable-http#standard-request-headers:
| Mcp-Name | params.name or params.uri | tools/call, resources/read, prompts/get requests |


3. Correctness

3.1 mcp_decode_header_value falls back to the raw string on decode failure

mcp.cc:548, fallback at mcp.cc:556.

spec:basic/transports/streamable-http#value-encoding: "To avoid ambiguity,
clients MUST also Base64-encode any plain-ASCII value that matches the
sentinel pattern" (trailing parenthetical elided).

Narrow but real. Because the decoded value is compared to the body value at
mcp.cc:730, a mis-decode normally just fails the match. The fallback only
wrongly accepts when the body value itself literally equals a sentinel-shaped
string, which no conformant client can send.

The decoder also accepts non-canonical input, demonstrable from
mcp.cc:531-533 (if (character == '=') break;): "=QQ" decodes to ""
rather than failing, "QQ=QQ" yields "A", and "QQ" and "QR" both yield
"A", so two encodings collide. It does correctly reject base64url, since
- and _ are outside the alphabet. That behavior is right and should be kept.

Two constraints on the fix. The size() >= 11 guard is necessary, not
redundant: in "=?base64?=" the ?= suffix overlaps the prefix's trailing ?,
so both starts_with and ends_with pass and raw.size() - 11 would
underflow. And the same helper is called on the legacy path at mcp.cc:767.
Neither 2025-11-25 nor 2025-06-18 defines Mcp-Name, Mcp-Param-* or the
sentinel, so on those revisions a name that happens to start with =?base64?
is just a name. Failing instead of falling back must be scoped to the
2026-07-28 call site at mcp.cc:729. Arguably mcp.cc:767 should not decode
at all.

3.2 ttl_ms is std::uint64_t but serialized through std::int64_t

MCPCachePolicy::ttl_ms (mcp.h:542), cast at mcp.cc:904 and mcp.cc:1307.
JSON::Integer is std::int64_t, and the only integer constructor is
explicit JSON(const std::int64_t), so the narrowing is real.

spec:server/utilities/caching#time-to-live-ttl-field: "Servers MUST
provide a ttlMs value that is >= 0."

Only values above INT64_MAX are affected, and since C++20 the conversion is
well-defined modular rather than undefined. The realistic trigger is a caller
using std::numeric_limits<std::uint64_t>::max() as a never-expires sentinel,
which emits a negative ttlMs that clients then treat as 0, the inverse of
the intent. The field's own doc comment already reads "Must be non-negative",
which is vacuous for an unsigned type. Type it as std::int64_t to match
JSON::Integer.

3.3 Asserted-only object preconditions

mcp.cc:850 and mcp.cc:902 assert result.is_object() and then mutate.
JSON stores its payload in an anonymous union, and defines, at, assign
and assign_assume_new each assert is_object() without a runtime check or
throw, so reading the inactive member under NDEBUG is genuine undefined
behavior. The window is narrow: both mutations sit behind an early return, so
it requires version == V_2026_07_28 (and, for the second, a cache policy).

3.4 _meta lookup discriminates on the mere presence of jsonrpc

mcp.cc:308. schema.ts types request params as
params?: { [key: string]: any }, so a top-level jsonrpc key is not
forbidden. A bare params object carrying one makes jsonrpc_params return
nullptr and yields a false MissingParams. Pathological input, but the
tightening is free: require jsonrpc == "2.0" as a string, matching
jsonrpc_is_notification. MCP_HASH_JSONRPC is already defined at
mcp.cc:63 and is not used here.

3.5 server/discover substitutes the library's versions for the server's

Two mechanisms, and both need removing. The default argument
const std::vector<JSON::StringView> &supported_versions = {} (mcp.h:1019),
and the if (supported_versions.empty()) fallback to
mcp_supported_protocol_versions() (mcp.cc:1259-1262). Dropping only the
default leaves the fallback substituting the library's list for any caller that
passes an empty vector.

schema.ts, DiscoverResult.supportedVersions: "MCP Protocol Versions this
server supports." (second sentence elided)

A server that implements only 2026-07-28 currently advertises 2025-03-26.

3.6 The version-mismatch branch emits -32020 where -32022 is required

mcp.cc:686: with version == V_2026_07_28 and a header naming a different
resolvable revision, it returns a header mismatch. But nothing mismatched. The
header and the body _meta can agree exactly, and the server simply does not
implement that revision.

spec:basic/transports/streamable-http#protocol-version-header: "If the
server does not implement the requested protocol version (whether the version
is unknown to the server, or is a known version the server has chosen not to
support), it MUST respond with 400 Bad Request and an
UnsupportedProtocolVersionError listing its supported versions."

spec:basic/index#error-codes: implementations "MUST use defined codes
only with their specified meanings."

Emit mcp_make_error_unsupported_protocol_version there instead. Split the two
outcomes: header disagrees with the body _meta gives -32020, header names a
revision this server does not implement gives -32022.

Keep the version parameter. It is what encodes that headers are required for
2026-07-28 and optional for the older revisions (mcp.h:752-755). Deriving the
revision from the client-supplied header instead would let a client send
MCP-Protocol-Version: 2025-03-26 to route into the legacy branch
(mcp.cc:745) and skip the required-Mcp-Method, required-Mcp-Name and
header-versus-_meta checks. It would also resolve against the library's four
versions rather than the server's, which is the same conflation as 3.5. If
anything, the parameter should become the server's supported-version set.

3.7 MCPCacheScope::Public is a silent default

MCPCachePolicy::scope defaults to MCPCacheScope::Public (mcp.h:544), and
mcp_make_server_discover_result defaults the whole policy to
{.ttl_ms = 3600000, .scope = MCPCacheScope::Public} (mcp.h:1021-1022).

spec:server/utilities/caching#security-considerations: "Servers MUST be
aware that responses with a "public" cacheScope may be shared between
callers even if the Result is coming from an authenticated endpoint ...
different access tokens can leverage the same cache." Server implementors
"should ensure that the cacheScope correctly reflects the intended
visibility of the primitive."

A caller who omits the policy gets a discovery result that is shareable across
authorization contexts for an hour. This default should not exist.

3.8 mcp_serialize_client_capabilities has no version gate

mcp.cc:271 emits roots.listChanged unconditionally, and the function takes
no version parameter (mcp.h:586). In 2026-07-28 that sub-field is gone,
schema.ts ClientCapabilities has only roots?: {};, consistent with
changelog major change 5 removing notifications/roots/list_changed. Reading
it in mcp_parse_client_capabilities (mcp.cc:231) stays correct for the older
revisions the module serves, so the gate belongs on the serializer.

Not a wire violation on its own, since the definition is open ("this is not a
closed set: any client can define its own, additional capabilities", and
schema.json sets no additionalProperties: false). It becomes one when the
output is used as the -32021 payload, which the spec types as
ClientCapabilities:

schema.ts, MissingRequiredClientCapabilityError:
data: { requiredCapabilities: ClientCapabilities; }.

mcp_serialize_client_capabilities is never called inside the module, and
mcp_make_error_missing_required_capability (mcp.cc:579) takes a pre-built
JSON, so this is latent until a consumer composes the two.


4. Conventions to check

Counts are against the merge base, over src/core/mcp and test/mcp
(3841 insertions, 270 deletions).

Rule Finding
Doxygen example required per public function The header goes from 17 cpp fences to 3. 15 are deleted and 1 added, so 15 pre-existing function examples are lost. Of 46 new mcp_* function names, exactly one (mcp_protocol_version_at_least) carries an example, leaving 45 new public functions with none, or 51 counting new overloads of existing functions separately
Spec citations in comments, with document, section and quote Zero spec URLs added. Greps for modelcontextprotocol.io, specification, SEP- and http across the new header and source return nothing outside test fixtures. Three existing citations are deleted: the transport URL plus its SHOULD paraphrase in mcp_resolve_protocol_version, the server/tools URL in mcp_make_tool_descriptor, and the basic/lifecycle#version-negotiation URL plus its verbatim MUST/SHOULD quote in mcp_make_initialize_result. The 2025-03-26 fallback is not unexplained, the new doxygen paraphrases it, but it is now unsourced and unquoted
Prefer try_at over defines plus at mcp.cc:878, and five pairs in mcp_request_subscription_id (mcp.cc:1414/1415, 1416/1417, 1419/1420, 1421/1422, 1424/1425). Lines 1419-1422 also use the string-only overloads, dropping the precomputed hash their siblings pass
Avoid unnecessary allocations mcp_supported_protocol_versions (mcp.cc:217) returns std::vector by value. Three library call sites: mcp.cc:683 and mcp.cc:752 on error paths, and mcp.cc:1260 on the server/discover success path, which is the same default faulted in 3.5. A std::span over a static array fits all three
Public API surface MCPRequestMetaStatus (mcp.h:591) has exactly 18 enumerators, forcing an exhaustive switch of that size on every consumer. A smaller status plus a field naming the offending key carries the same information, and would subsume 2.2

5. API shape

5.1 The version-defaulting overloads are a compliance hazard

Four overloads silently substitute V_2025_11_25 when the caller omits the
version:

Overload Defaults to
mcp_make_tool_error(identifier, message) (mcp.cc:980) V_2025_11_25
mcp_make_resources_read_result(contents) (mcp.cc:1047) V_2025_11_25
mcp_make_error_resource_not_found(identifier) (mcp.cc:790) V_2025_11_25, so -32002
mcp_is_request_method(method) (mcp.h:255) V_2025_11_25

Any call site not updated keeps compiling and silently produces output for the
wrong revision. Two of the four are outright violations rather than cosmetic:

Row 3 emits a code this revision forbids.

spec:basic/index#error-codes: "Codes defined by earlier protocol versions
remain reserved and will not be reused. Implementations of this protocol
version MUST NOT emit these codes:" followed by a list whose entries are
-32002 (resource not found) and -32042 (URL elicitation required).

Row 4 makes a server reject a method it is required to implement.

spec:server/discover: "server/discover lets a client query a server's
supported protocol versions, capabilities, and identity before sending any
other requests. Servers MUST implement it."

Since compatibility is not a goal, delete all four and make the version
parameter mandatory. A compile error at every call site is the desired outcome.

5.2 The decorators cannot decorate a result the caller must retain

mcp_decorate_result (mcp.h:813) and mcp_decorate_cacheable_result
(mcp.h:822) take sourcemeta::core::JSON by value, and no borrowing overload
exists.

For a caller that owns the result outright this costs nothing, because JSON
has a noexcept move constructor and move assignment, so
result = mcp_decorate_result(version, std::move(result), info); is two moves
and no deep copy. The cost lands only on a caller holding a result it must keep,
such as a cached or otherwise shared result that is decorated per use. There the
by-value signature forces a full copy.

The fix cannot be an overload taking JSON & beside the existing one. For an
lvalue argument both candidates are exact matches and the call becomes
ambiguous. Either give the in-place form a distinct name, or, since
compatibility is not a goal here, replace the by-value form with it and move at
the call sites that own their result.

The part-wise builders (mcp_make_resources_list_result and siblings) do not
cover this case, since they rebuild the result from its components.

@HarshPopat23

Copy link
Copy Markdown
Contributor Author

Thanks a ton for running this simulation against Sourcemeta One, @jviotti This is pure gold.

Here is how I propose breaking down the implementation to address everything cleanly:

1.Spec Fixes:

  • Restore clientInfo as optional in _meta (while keeping strict string name/version checks when present).
  • Support string | number for subscriptionId and derive the close result ID from the request identifier.
  • Reject empty protocolVersion in _meta as -32022 (unsupported version).
  • Require explicit MCPCachePolicy on 2026 cacheable builders (drop the silent Public default) and type ttl_ms as int64_t.
  • Constrain InputRequiredResult to allowed methods (tools/call, resources/read, prompts/get) and require at least one payload field.
  • Reject malformed Base64 headers on 2026-07-28 rather than falling back to raw strings.

2.Sourcemeta One Ergonomics & Alignment:

  • Complete the 2026-07-28 method table (adding prompts/get, prompts/list, etc.) to align with mcp_is_named_request_method.
  • Preserve sub-capabilities in MCPClientCapabilities (sampling, elicitation) and gate roots.listChanged.
  • Add helpers: mcp_make_error_request_meta, mcp_error_code_to_http_status, mcp_make_empty_result, and an in-place mcp_decorate_result overload.
  • Remove legacy-defaulting overloads to prevent accidental version mismatch.

3.Conventions & Docs:

  • Add Doxygen examples and exact spec citations across mcp.h.
  • Replace defines + at pairs with try_at.

Does this plan look good to you to proceed with the updates ?

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants