mcp: add MCP 2026-07-28 protocol support - #2878
HarshPopat23 wants to merge 3 commits into
Conversation
Signed-off-by: HarshPopat23 <musichk61@gmail.com>
🤖 Augment PR SummarySummary: This PR begins support for MCP protocol revision Changes:
Technical Notes: The change intentionally excludes the broader 2026-07-28 feature and transport upgrades. 🤖 Was this summary useful? React with 👍 or 👎 |
| // https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#protocol-version-header | ||
| return MCPProtocolVersion::V_2025_03_26; | ||
| } | ||
| if (header == "2026-07-28") { |
There was a problem hiding this comment.
[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
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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().
|
PTAL : @jviotti |
|
Looks good so far, but merging this as-is, without the other changes, would make it tricky for i.e. Can we try to land support for the new version all in one shot? |
|
Why not @jviotti , |
There was a problem hiding this comment.
All reported issues were addressed across 3 files (changes from recent commits).
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
d07653c to
c32456f
Compare
Signed-off-by: HarshPopat23 <musichk61@gmail.com>
c32456f to
11c9459
Compare
|
@jviotti |
Signed-off-by: HarshPopat23 <musichk61@gmail.com>
There was a problem hiding this comment.
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
| if (!resolved_protocol.has_value()) { | ||
| return mcp_make_error_unsupported_protocol_version( | ||
| request_id, protocol_version_header.value(), | ||
| mcp_supported_protocol_versions()); | ||
| } |
There was a problem hiding this comment.
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>
| 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)); | |
| } |
| -> std::optional<std::string> { | ||
| std::string decoded; | ||
| decoded.reserve(encoded.size() * 3 / 4); | ||
| int accumulator = 0; |
There was a problem hiding this comment.
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>
| 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. |
There was a problem hiding this comment.
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>
| /// `Mcp-Method`, and optionally `Mcp-Name` for named methods) are required. | |
| /// `Mcp-Method`, and `Mcp-Name` for named methods) are required. |
|
@jviotti ,
|
|
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 violations1.1
|
| 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
nameandversion; 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 thesubscriptions/listenrequest."
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-RPCidof the originatingsubscriptions/listenrequest."
schema.ts,SubscriptionsListenResultMetaObject: "(and equals this
response'sid)".
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 withresultType: "complete"returned by the
following operations:", followed by a list ofserver/discover,
tools/list,prompts/list,resources/list,
resources/templates/listandresources/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"cacheScopemay 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 theMCP-Protocol-Versionheader)
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 ofinputRequestsorrequestState
in everyInputRequiredResultresponse."
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
InputRequiredResultonprompts/get,resources/readandtools/call,
then "Servers MUST NOT sendInputRequiredResultresponses 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 recognizedMcp-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 a400 Bad RequestHTTP 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 ofx-mcp-headeris 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 be400 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 with400 Bad Requestand anUnsupportedProtocolVersionError
listing its supported versions."Same section: "If the server does not implement the requested RPC method, it
MUST respond with404 Not Foundand 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: "TheresultMUST include a
resultTypefield 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 aninputRequeststhat 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 attlMsvalue 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 with400 Bad Requestand an
UnsupportedProtocolVersionErrorlisting 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"cacheScopemay 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 thecacheScopecorrectly 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/discoverlets 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.
|
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:
2.Sourcemeta One Ergonomics & Alignment:
3.Conventions & Docs:
Does this plan look good to you to proceed with the updates ? |
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)
MCPProtocolVersion::V_2026_07_28("2026-07-28") enum, string parsing, serialization, and chronological ordering.mcp_uses_initialization_handshake()returnsfalsefor 2026-07-28; legacyinitializenegotiation correctly falls back to2025-11-25viamcp_latest_initialization_version()._meta): Strict validation viamcp_validate_request_meta:io.modelcontextprotocol/protocolVersion).io.modelcontextprotocol/clientCapabilities).io.modelcontextprotocol/clientInfowith required stringnameandversion, plus optionaltitle/descriptionstring type validation).mcp_request_protocol_version,mcp_request_client_info,mcp_request_client_capabilities,mcp_has_required_request_meta).mcp_validate_request_headers:MCP-Protocol-Versionmatching request_meta.Mcp-Methodmatching JSON-RPC body method.Mcp-Nameon named requests (tools/call,resources/read,prompts/get), including RFC 9110 Base64 header decoding (=?base64?...?=).Mcp-Nameon unnamed requests.2025-03-26,2025-06-18,2025-11-25).-32020(Header mismatch) with structured header/body details.-32021(Missing required client capability) withrequiredCapabilitiespayload.-32022(Unsupported protocol version) with requested/supported versions list.-32602(Invalid params): version-aware resource not found error for 2026-07-28 (preserving-32002for legacy revisions).server/discover): Discovery response builder (mcp_make_server_discover_result) returning supported versions, server capabilities, and server info under_meta.resultType: "complete"(mcp_decorate_result).🟡 Optional / Extended Protocol Primitives (Included in this PR)
ttlMsandcacheScope(public/private) emitted for cacheable results (tools/list,resources/list,resources/templates/list).resultType: "input_required", optionalinputRequests, and opaquerequestState.subscriptions/listenandnotifications/subscriptions/acknowledged.extensionsandexperimentalmaps.⏩ Subsequent PRs & Follow-ups
sourcemeta/one.Backward Compatibility
2025-03-26,2025-06-18,2025-11-25) remain byte-for-byte identical in behavior and serialized output.Testing & Quality
test/mcp/mcp_test.cc(covering all positive, negative, and edge cases).cmake --build build --target clang_format_test).git diff --check).