Skip to content

Commit d683488

Browse files
committed
feat(protocol-2026): re-pin DRAFT-2026-v1 schema to 6e4cba2d, fix wire defects
Re-vendors schema.ts (1bf94a60 -> 6e4cba2d) and the upstream fixture pin (1304c8fe -> 60dc69e9) to close the drift between the 2026-06-10 pin and the live draft. Applies every wire-breaking and binding change carried by the diff: - MCP error codes renumbered into the schema's now-partitioned ranges (-32000..-32019 implementation-defined, -32020..-32099 spec-reserved): HeaderMismatch/MissingRequiredClientCapability/UnsupportedProtocolVersion move to -32020/-32021/-32022; framework-internal McpError codes that squatted those numbers renumbered into -32000..-32019. New mcp_error_code_partition test pins both invariants; six turul-mcp-server test files with hardcoded old codes migrated. - CancelledNotificationParams.requestId reverted to required (was Option), client->server only. - ElicitationCompleteNotification/Params and ElicitRequestURLParams .elicitationId removed entirely. - New NotificationMetaObject / SubscriptionsListenResult(+Meta) types for subscription-stream metadata; subscriptionId stamping was already implemented server-side ahead of the spec formalizing it. - ListRootsRequest.params reverted to a bespoke inline shape. Also fixes two defects found during re-audit, not schema-diff-driven: - ContentBlock::ResourceLink emitted duplicate annotations/_meta keys on the wire (variant fields alongside the flattened ResourceReference, both carrying the same fields) - removed the variant-level fields. - PrimitiveSchemaDefinition's untagged enum didn't enforce the schema's `type` discriminator (bare {"type":"integer"} silently parsed as StringSchema) - added a hand-written Deserialize that dispatches on type/enum/oneOf before trying variants. - SubscriptionsListenResultMeta's caller-writable `extra` map could shadow the typed subscriptionId field with a colliding key - hand-written Serialize now drops any colliding extra entry. All new/changed contracts covered by regression tests confirmed failing before the fix and passing after. The ResourceLink and SubscriptionsListenResultMeta duplicate-key tests assert against raw serialized text rather than serde_json::Value, since Value cannot represent duplicate keys and would silently pass either way. 376 tests pass under --features compliance (185 lib + 187 integration + 3 fixture + 1 doctest), 366 default. clippy -D warnings clean. ADR-027 revision log records the full diff and fix rationale. COMPLIANCE.md documents the two disclosed-not-fixed gaps: no shutdown-signal path exists yet to emit SubscriptionsListenResult, and RequestMetaObject.extra carries the same reserved-key-collision risk just fixed on SubscriptionsListenResultMeta.
1 parent d2c5fa3 commit d683488

34 files changed

Lines changed: 847 additions & 415 deletions

crates/turul-http-mcp-server/src/server.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -519,7 +519,7 @@ async fn handle_request(
519519

520520
// The 2026 stateless build serves a single spec: every request goes to the
521521
// streamable handler, whose Server Validation rejects unsupported
522-
// MCP-Protocol-Version values with 400 + -32004. Routing legacy version
522+
// MCP-Protocol-Version values with 400 + -32022. Routing legacy version
523523
// headers to the 2025-era session handler would bypass that contract.
524524
#[cfg(feature = "protocol-2026-07-28")]
525525
let route_streamable = true;

crates/turul-http-mcp-server/src/streamable_http.rs

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1394,7 +1394,7 @@ impl StreamableHttpHandler {
13941394
// pre-2025-06-18 clients, so an absent header is rejected) and Mcp-Method
13951395
// matching the body method; tools/call, resources/read, and prompts/get
13961396
// additionally require Mcp-Name matching params.name / params.uri.
1397-
// Failures → HTTP 400 + JSON-RPC -32001 HeaderMismatch (id-less when the
1397+
// Failures → HTTP 400 + JSON-RPC -32020 HeaderMismatch (id-less when the
13981398
// body is a notification). Header names compare case-insensitively
13991399
// (hyper lowercases them in `context.headers`); values are case-sensitive.
14001400
#[cfg(feature = "protocol-2026-07-28")]
@@ -1408,7 +1408,7 @@ impl StreamableHttpHandler {
14081408

14091409
// Version negotiation: a requested version this build does not
14101410
// implement (unknown or known-but-unsupported) → 400 +
1411-
// UnsupportedProtocolVersionError (-32004) listing the supported set.
1411+
// UnsupportedProtocolVersionError (-32022) listing the supported set.
14121412
if let Some(requested) = header("mcp-protocol-version")
14131413
&& requested != turul_mcp_protocol::MCP_VERSION
14141414
{
@@ -1656,7 +1656,7 @@ impl StreamableHttpHandler {
16561656
// with protocolVersion/clientInfo/clientCapabilities (schema: RequestParams._meta
16571657
// is required). Missing/incomplete `_meta` → -32602 (HTTP 400); a `_meta`
16581658
// protocolVersion that disagrees with the (already validated) header is a
1659-
// header-validation failure → -32001 HeaderMismatch (HTTP 400).
1659+
// header-validation failure → -32020 HeaderMismatch (HTTP 400).
16601660
#[cfg(feature = "protocol-2026-07-28")]
16611661
if let JsonRpcMessage::Request(req) = &message {
16621662
let v = serde_json::to_value(req).unwrap_or(serde_json::Value::Null);
@@ -1912,7 +1912,11 @@ impl StreamableHttpHandler {
19121912
/// - Every delivered notification carries
19131913
/// `io.modelcontextprotocol/subscriptionId` in `_meta`, set to the JSON-RPC
19141914
/// id of the listen request.
1915-
/// - The client cancels by closing the stream; there is no JSON-RPC result.
1915+
/// - Client-initiated cancellation (closing the stream) carries no JSON-RPC
1916+
/// result. Schema also defines `SubscriptionsListenResult` for a
1917+
/// server-initiated graceful teardown (e.g. shutdown) — this handler has
1918+
/// no such teardown path yet, so that result is never emitted; see
1919+
/// `turul_mcp_protocol::subscriptions::SubscriptionsListenResult`.
19161920
#[cfg(feature = "protocol-2026-07-28")]
19171921
async fn handle_subscriptions_listen(
19181922
&self,
@@ -2216,7 +2220,7 @@ impl StreamableHttpHandler {
22162220
let connection_id = format!("post-{}", uuid::Uuid::now_v7().as_simple());
22172221

22182222
// 2026 + JSON framing: dispatch inline so the HTTP status can reflect
2219-
// the JSON-RPC outcome. MissingRequiredClientCapabilityError (-32003)
2223+
// the JSON-RPC outcome. MissingRequiredClientCapabilityError (-32021)
22202224
// MUST ride HTTP 400 per its schema; the chunked-channel path below
22212225
// commits 200 before dispatch completes and cannot retro-status.
22222226
// (SSE-framed responses inherently stay 200 — errors ride the stream.)
@@ -2231,10 +2235,13 @@ impl StreamableHttpHandler {
22312235
)
22322236
.await;
22332237
let status = match &response {
2234-
// -32003 MissingRequiredClientCapability and -32001
2238+
// -32021 MissingRequiredClientCapability and -32020
22352239
// HeaderMismatch both mandate HTTP 400.
22362240
turul_rpc::JsonRpcResponse::Error(err)
2237-
if matches!(err.error.code, -32001 | -32003) =>
2241+
if matches!(
2242+
err.error.code,
2243+
turul_mcp_protocol::headers::ERROR_CODE_HEADER_MISMATCH | -32021
2244+
) =>
22382245
{
22392246
StatusCode::BAD_REQUEST
22402247
}

crates/turul-mcp-builders/src/notification.rs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -498,12 +498,10 @@ mod tests {
498498
.build();
499499

500500
assert_eq!(notification.method, "notifications/cancelled");
501-
// CancelledNotificationParams.request_id is a bare RequestId under 2025-11-25
502-
// and Option<RequestId> under 2026-07-28.
503-
#[cfg(feature = "protocol-2025-11-25")]
501+
// CancelledNotificationParams.request_id is a required bare RequestId
502+
// on both lanes (the 2026-07-02 re-pin reverted the 2026-05-31
503+
// Option<RequestId> relaxation on the 2026-07-28 lane).
504504
assert_eq!(notification.params.request_id, RequestId::Number(123));
505-
#[cfg(feature = "protocol-2026-07-28")]
506-
assert_eq!(notification.params.request_id, Some(RequestId::Number(123)));
507505
assert_eq!(
508506
notification.params.reason,
509507
Some("User cancelled operation".to_string())

0 commit comments

Comments
 (0)