0.4.0 — adopt MCP 2026-07-28 as the default spec (2025-11-25 becomes the opt-in lane) - #18
Merged
Merged
Conversation
Initial commit of the DRAFT-2026-v1 protocol crate source. Comments describe interfaces by name (not upstream schema line numbers, which rot on re-pin) and carry no internal Phase/Slice tags. Per CLAUDE.md section Comments. Cargo.toml, tests/, and supporting docs (ADRs 027/028, compliance plans) intentionally not included -- separate slices.
…l-rpc 0.2.1 Schema-driven cleanup of the 2026-07-28 protocol crate's RPC layer. Wire envelopes now come from `turul-rpc` 0.2.1 directly (dropped the turul-mcp-json-rpc-server shim per ADR-025). Drops duplicate JsonRpcRequest/Response/Notification/Error/Message types from our crate in favor of upstream's schema-compliant union: - JsonRpcResponse is the schema's `JSONRPCResponse = Success | Error` union - JsonRpcSuccessResponse is the success-only struct - JsonRpcError is the error response envelope with optional id - RequestId is typed `String | Number` - JsonRpcVersion is typed MCP RequestParams shape brought to schema compliance: - RequestParams._meta is required (was Option) — schema requires per-request capability negotiation in DRAFT-2026-v1 stateless core - PaginatedRequestParams._meta is required (inherited) - NotificationParams is a distinct type with optional _meta (per schema) - *Request::new() constructors now take RequestMetaObject — Default impls removed where they would produce non-spec wire shapes Trait redesign per the schema's Result interface: - RpcResult: HasMeta + HasResultType (was HasMeta + HasData, which forced HashMap<String, Value> round-trips for every typed Result) - HasMeta::meta() -> Option<&MetaObject> (typed borrow, no clone) - HasResultType added — every spec-compliant Result exposes its discriminator - HasData kept as escape hatch, no longer in the supertrait bound - HasOptionalRequestId added (schema's JSONRPCErrorResponse.id?) - JsonRpcResponseTrait split into JsonRpcResultResponseTrait + JsonRpcErrorResponseTrait matching the schema's response union - CreateMessageResult correctly no longer impls RpcResult (extends SamplingMessage, not Result, per schema) ResultWithMeta removed — zero production callers after the migration; typed result structs carry their own data; envelope result uses turul_rpc::ResponseResult. 322 tests pass (139 lib + 179 compliance + 3 upstream-fixtures + 1 doctest), zero warnings. Initial commit of crate manifest, README, schema vendor, and tests (previously untracked — were not part of the docs-only first commit).
…rs, remove non-spec fields Schema audit of typed `*RequestParams` types — completes the `_meta`-required cascade started by the previous slice's flip on `RequestParams` + `PaginatedRequestParams`. Wave 1 — _meta now required (per schema's extension chain): - CallToolRequestParams (extends InputResponseRequestParams extends RequestParams) - GetPromptRequestParams (same) - ReadResourceRequestParams (extends ResourceRequestParams, InputResponseRequestParams) - CompleteRequestParams (extends RequestParams) All four had `meta: Option<RequestMetaObject>`. The schema marks `RequestParams._meta` as required (no `?`), so the field is now non-Option. `*::new()` constructors now take `RequestMetaObject` as their second argument; `HasMetaParam` impls reach into the typed `extra` map directly. Wave 2 — non-spec fields removed (Protocol Crate Purity): - ElicitRequestFormParams.meta — schema interface has NO `_meta` field - ElicitRequestURLParams.meta — schema interface has NO `_meta` field - CreateMessageRequestParams.meta — schema interface has NO `_meta` field These structs do NOT extend `RequestParams` per the schema. The Rust `meta` field was carryover from earlier work and violated §"Protocol Crate Purity". Removed entirely along with their `HasMetaParam` impls and stale `with_meta`/builder calls. Test rewrites: - `test_optional_meta_serialization` → `test_meta_always_serialized` (asserts the schema-correct invariant: `_meta` is required and present on every `tools/call` wire payload) - `test_elicit_request_matches_typescript_spec` rewritten to assert `_meta` is ABSENT on ElicitRequestFormParams (schema says it shouldn't be there) — a positive regression guard against the non-spec field being added back 322 tests pass, zero warnings.
The previous slice bumped workspace `turul-rpc = "0.2.1"`, which broke `turul-mcp-json-rpc-server` (the 0.3.x shim) and its downstream consumers (`turul-http-mcp-server` etc.) — those were written against the 0.1 API where `JsonRpcMessage::Error` existed before 0.2's rename. The shim re-exports turul-rpc transparently, so consumers see whatever version the workspace pins. Resolution: workspace stays on `turul-rpc 0.1`. The 2026-07-28 protocol crate pins `turul-rpc = "0.2.1"` directly in its own Cargo.toml. Both versions coexist in the Cargo.lock; only the 2026-07-28 line sees 0.2. `cargo check --workspace` clean; 2026-07-28 tests still pass (322).
… drift
Schema-driven cleanup of the 2026-07-28 protocol crate. Covers:
- turul-rpc bumped to 0.2.2, picks up JsonRpcWireMessage (the 3-variant
JSONRPCMessage union) and re-exports it as the wire-direction-agnostic
frame type.
- Contract fix: HasCallToolRequestParams::arguments() returned None
unconditionally despite the field being populated. Trait signature
changed from Option<&Value> to Option<&HashMap<String, Value>>
(matches the underlying field shape and the schema's
arguments?: { [key: string]: unknown }).
- Compat traits for methods removed in DRAFT-2026-v1 deleted from
traits.rs: InitializeRequest, InitializeResult, InitializeNotification,
HasInitializeParams, RootsListChangedNotification. Schema-only API
per Protocol Crate Purity; stateless core has no initialize handshake
(per-request RequestMetaObject + DiscoverResult replace it) and roots
carries no list_changed notification.
- COMPLIANCE.md added: pinned schema SHA, per-symbol coverage matrix,
method-string verification, wire-field name table, @see anchor coverage,
fixture roundtrip status (8 modeled / 20 fixture files / 322 tests),
intentional deviations.
- Doc/comment cleanup applied across src/ and tests/:
- schema line refs replaced with interface names (line numbers rot on
re-pin; names survive)
- phase/slice/group session tags removed
- tombstone narratives ("X removed: ...") replaced with forward-state
descriptions or deleted where redundant
- stale "MCP 2025-11-25 specification" wording replaced with current-spec
statements; legitimate migration notes, version-enum literals, and
cross-version test data retained
- module-level docs rewritten where they described removed handshake
behaviour as current (initialize.rs, notifications.rs)
- 5 broken intra-doc links fixed; cargo doc --no-deps now clean
- @see anchors mirrored on MetaObject, RequestMetaObject, and JsonRpcError
per upstream schema TypeDoc tags (URL fragments survive re-pins).
- icons.rs stale 2025-11-25 spec URLs replaced with name-anchored prose.
Verification gate (run before declaring done, per CLAUDE.md
"Slice Completion Gate"):
- grep across crate for Schema line / Phase / Slice / Group / CLAUDE.md
in .rs / tombstones / removed-method strings: all 0
- 22 remaining 2025-11-25 hits dispositioned: McpVersion enum literals
(load-bearing code), test data (positive cross-version assertions),
migration notes explaining current shapes
- cargo build --features compliance: clean
- cargo test --features compliance: 322 passing, 0 failed, 0 warnings
- cargo doc --no-deps: 0 warnings
Scope: protocol crate only. Workspace governance (AGENTS.md / CLAUDE.md
branch-target marking) and Cargo.toml version churn handled separately.
…ons + test allows Combines Slice A' (eight schema-fidelity corrections), Slice A'' (SEP-2577 deprecation annotations + duplicate LoggingMessageNotification reconcile), and Slice D test-attribute additions into one protocol-crate-scoped commit. A1-A8 (Slice A'): - A1: meta::ProgressToken unified to untagged `string | serde_json::Number` - A2: SubscriptionsListenRequestParams._meta upgraded to required RequestMetaObject - A3: CancelledNotificationParams.request_id relaxed to Option<RequestId> - A4: ContentBlock::ResourceLink gains size + icons (ResourceLink extends Resource) - A5: ElicitationSchema gains $schema field (JSON Schema 2020-12 dialect) - A6: ListRootsRequest.params -> Option<RequestParams> (drop bespoke struct) - A7: Seven notification traits rebound RpcNotification (was unimplementable) - A8: New DiscoverRequestTrait, SubscriptionsListenRequestTrait, HasInputRequiredResult SEP-2577 annotations (Slice A''): - #[deprecated(since="0.4.0", note="...")] on Roots, Sampling, Logging types - LoggingLevel intentionally NOT deprecated (replacement value type) - Pre-existing duplicate LoggingMessageNotification removed from logging.rs Test-attribute additions (Slice D test-attrs, fixing P0-1 warning leak): - #[allow(deprecated)] on tests in notifications.rs, input_required.rs, elicitation.rs - #![allow(deprecated)] crate-level on tests/compliance.rs - 342 tests pass, 0 deprecation warnings See COMPLIANCE.md sections "Schema-fidelity corrections (Slice A')" and "SEP-2577 deprecation annotations (Slice A'')".
…ngelog Slice B-1: governance documentation for the dual-branch (main on 2025-11-25, feat-branch on DRAFT-2026-v1) work-in-progress state. - CLAUDE.md: §Branch Lock, §Frozen Protocol Crates, §Crate Versioning Policy, §Comments, §Slice Completion Gate, §Reviewing Agents added. - AGENTS.md: new §"Branch-Conditional Spec Guidance" + section markers on the 9 unqualified 2025-11-25 lines per branch-scope. - CHANGELOG.md: 0.4.0 entry covering the new turul-mcp-protocol-2026-07-28 crate, per-crate independent versioning, frozen-crate carve-out, and Notes for downstream consumers (Phase 9.4 in-scope, not deferred). - docs/adr/027-targeting-mcp-draft-2026-v1.md (NEW): wire-string target, schema regeneration trigger, per-crate versioning, Status update (0.4.0 default = DRAFT-2026-v1). - docs/adr/028-extensions-strategy.md (NEW): per-extension turul-mcp-ext-* crates per official MCP extensions; reverse-DNS validation; independent semver per extension crate. ADR-027 carries the post-Slice-C amendments (§Consequences replaced, §Status update inserted) so the file lands here in its current operative state. Subsequent commits do not modify it.
…-* at 0.3.47 Slice B-2: every member Cargo.toml migrated from version.workspace = true to a literal version field. The frozen 2025-06-18 and 2025-11-25 protocol crates pin to 0.3.47 indefinitely (one-time exception documented in ADR-027 revision log); all other non-frozen crates ship 0.4.0 as the first per-crate independent-versioning release. - Root Cargo.toml: [workspace.package].version = "0.4.0" (default for tooling that still reads it; not authoritative). - crates/turul-mcp-protocol-2025-06-18/Cargo.toml: literal "0.3.47" + self- contained comment explaining the freeze. - crates/turul-mcp-protocol-2025-11-25/Cargo.toml: literal "0.3.47" + same. - All other crates/*/Cargo.toml: literal "0.4.0". - examples/*/Cargo.toml + tests/*/Cargo.toml: literal "0.4.0". Bisectable as a single atomic commit so the workspace-wide cut is one revertable unit. After this commit: cargo build --workspace -> clean cargo test --workspace -> 1727+ tests pass (no per-crate version skew) Policy details in CLAUDE.md §"Crate Versioning Policy" and the ADR-027 revision-log entry dated 2026-05-31 (per-crate independent versioning adoption).
…ex 4-pass remediation)
Slice C (load-bearing decision documentation) + four codex remediation
passes (Slices D-doc-edits, E, F, G) consolidated into one docs commit.
New decision ADRs:
- ADR-029 spec-coexistence-via-cargo-features: 0.4.0 default = DRAFT-2026-v1
via mutually-exclusive cargo features at the turul-mcp-protocol re-export
boundary; legacy-2025-11-25 as opt-in; flip-all-at-once Phase 9.4 strategy
committed; compile_error! mutex; cascade rule mandates default-features
= false on every consumer (codex P0-2 corrected); CI command targets leaf
crate features (codex P1 second-pass corrected).
- ADR-030 turul-mcp-client-bilingual-spec-coexistence: explicit
client-bilingual default feature (codex P0 second-pass after the
implicit-default design was found non-executable for optional deps);
--no-default-features required for narrowing; fallback triggers ONLY on
JSON-RPC -32601 (codex P1-5 security-adjacent fix — HTTP 4xx no longer
silently downgrades).
Six existing-ADR amendments (each preserves prior Status/Decision blocks):
- ADR-001 Lambda: §"Stateless mode (2026-07-28)" (~50 LOC vs ~200 LOC)
- ADR-006 Streamable HTTP: §"DRAFT-2026-v1 Stateless variant; GET SSE is 2025-only"
- ADR-009 Handler Routing: McpProtocolVersion becomes feature-exclusive
- ADR-023 Tool Change Detection: per-request fingerprint in stateless mode
- ADR-025 Extract turul-rpc: revision-log entry only
- ADR-026 Lambda Empty-Body: revision-log entry (moot in 2026 mode)
Plan docs (NEW):
- docs/plans/2026-07-28-{PARKED,architecture-review,codex-review-summary,
compliance-plan,example-fixture-compliance,feature-gating-rollout,
migration-diff,schema-coverage-matrix}.md
Codex review passes consolidated into the above:
- Slice D (first-pass P0-P2): doc edits across ADR-027/029/030,
CHANGELOG, PARKED, codex-review-summary, COMPLIANCE.md, feature-gating-
rollout. Test-attribute additions live in commit 527ad01 (Slice A''+D-
test-attrs). The wire_compliance.rs comment fix is the next commit.
- Slice E (second-pass): ADR-030 cargo design re-correction (after the
P1-4 implicit-default design was found non-executable); Phase 9.4 drift
mop-up across 4 docs.
- Slice F (third-pass): rollout plan Phase 4 + ADR-030 narrowing shorthand
cleanup; mutex verification commands expanded to triple-pair check.
- Slice G (fourth-pass P3): revision-log hygiene — SUPERSEDED/OPERATIVE
labels so historical revision-log entries don't grep-collide with operative
content.
0.4.0 NOT shippable to crates.io from this state; Phase 9.4 cutover work +
workspace turul-rpc 0.1->0.2.2 bulk pin + legacy CI E2E coverage are the
prerequisites scoped in feature-gating-rollout.md.
342 tests pass, 0 deprecation warnings, 0 doc warnings.
Slice D P2-6 (codex first-pass cleanup): the comment at
crates/turul-mcp-client/tests/wire_compliance.rs:195 cited CLAUDE.md by name
("These are wire-layer tests per CLAUDE.md rule 3"), which violates the
§Comments rule prohibiting playbook self-references in .rs source files.
Rewritten to describe the wire-layer-test concept directly without citing
the repo playbook. The rule itself (transport-protocol-boundary bytes vs
framework-internal state) is preserved in the comment text; only the
"per CLAUDE.md" attribution is removed.
Stays on the turul-mcp-client side of the dirty tree (not the protocol crate),
hence a standalone commit.
…e string Re-vendor draft-schema.ts (ETag 0eeaed15…, content sha256 20df36f9…). The export surface and 22 method strings are identical to the prior pin (stateless core intact). Three substantive wire changes: - MCP_VERSION / McpVersion serde rename "DRAFT-2026-v1" -> "2026-07-28" (draft literal kept as a deserialize-only alias for back-compat) - ResultType is now an open union; modeled as ResultType::Other(String) so unknown discriminators round-trip instead of being rejected - DiscoverResult extends CacheableResult (ttlMs/cacheScope required) Also resolves the clippy large_enum_variant gate on the deprecated MRTR InputRequest/InputResponse unions. Contract-change tests migrated; revert-and-fail verified for the version and ResultType deltas. 343 tests pass under --features compliance, 333 default; clippy -D warnings clean.
…o the JsonRpcResponse union Move the workspace turul-rpc pin 0.1 -> 0.2 (published, crates.io) and collapse the protocol-2026-07-28 per-crate 0.2.2 override onto it. turul-rpc 0.1 is no longer referenced on this branch (Cargo.lock resolves only 0.2.2). turul-rpc 0.2 makes JsonRpcMessage parse-only (Request/Notification) and moves outbound responses to the JsonRpcResponse Success/Error union. turul-http-mcp-server ported: dispatch produces JsonRpcResponse and converts to JsonRpcMessageResult via the canonical Success->Response / Error->Error mapping; JsonRpcResponse::success takes ResponseResult so result values are .into()-converted. Wire format is byte-identical; zero test expectations changed. Frozen 2025-* crates compile unchanged on 0.2. Workspace builds; http-server 92 unit + 11 doc tests pass; clippy -D warnings clean.
…mments CLAUDE.md §Comments and AGENTS.md §Coding Style now explicitly forbid ADR citations, internal phase/slice tags, and decision-history narratives in source (.rs and Cargo.toml) comments — comments describe the code as-is and what is non-obvious to a human. External MCP spec anchors (SEP-####, schema @see links) remain allowed since they name the wire contract the code implements.
- ADR-025: branch standardized on published turul-rpc 0.2; 0.1 removed from this branch (it was maintained only for the 0.3 framework on main) - ADR-027: add publication-gate condition (d) — one client binary must connect to both a 2026-07-28 and a 2025-11-25 server, with an acceptance test - ADR-028: active extension crates drop the schema-version suffix (turul-mcp-ext-tasks); build it and reconcile the cross-spec task models - ADR-029: symmetric spec-version feature names (protocol-2025-11-25, not legacy-); Cargo default stays at protocol-2026-07-28; title cleanup - ADR-030: client dual-spec captured as a release gate; dependency feasibility confirmed; turul-rpc single-major moots the two-majors concern Renamed legacy-2025-11-25 -> protocol-2025-11-25 across ADRs + the rollout plan (doc-only; the features do not exist in code yet).
Moves Claude Code workflow scripts and review reports out of repo content into .claude/ (gitignored). .claude/agents/ stays tracked (shared subagents).
…egotiation rule First step of the bilingual client (ADR-030). Additive and non-breaking; existing 2025-11-25 code paths are untouched. - Cargo: client-bilingual (default) links both versioned protocol crates; client-2025-only / client-2026-only narrow to one (require --no-default-features). Mutually exclusive, enforced by compile_error! in lib.rs (both footguns verified). - version.rs: McpVersion enum + classify_probe() — the per-connection negotiation decision rule. JSON-RPC -32601 is the ONLY downgrade signal (server lacks server/discover => 2025); HTTP 4xx and all other JSON-RPC errors abort rather than silently downgrade; opt-in legacy-gateway 404/405 hatch. 6 unit tests. Negotiation primitives are not yet wired into connect() (next step) — scoped #[allow(dead_code)] until then. All feature configs build; clippy -D warnings clean.
connect() now negotiates the wire spec per connection (ADR-030): - probe server/discover; a valid result locks 2026-07-28 - JSON-RPC -32601 falls back to the initialize handshake and locks 2025-11-25 - HTTP 4xx and all other JSON-RPC errors abort without downgrade (opt-in allow_legacy_gateway_fallback broadens to 404/405) - explicit ClientConfig.mcp_protocol_version hint skips the probe McpClient gains a per-connection locked version (negotiated_version()) and the narrowing client-2025-only / client-2026-only builds drive their handshake directly. Test mocks (MockTransport, StatefulMockTransport, wire_compliance wiremock servers) now answer server/discover with -32601 like a real 2025-11-25 server, so the existing fallback path is exercised end-to-end. 136 client tests pass; clippy -D warnings clean on all three feature configs.
…26 vs 2025 Three full-stack wiremock tests proving the stated minimum: a single McpClient (default bilingual build) locks 2026-07-28 against a server that answers server/discover, locks 2025-11-25 against a server that returns -32601, and aborts with no version locked when the probe gets HTTP 403 (no silent downgrade).
…t round-trips on both specs A 2026-locked connection now sends 2026-shaped requests (per-request _meta) and parses 2026-shaped results via the new protocol/v2026 module. fetch_tools() dispatches on the negotiated version; lock_version marks a stateless 2026 connection Active (it has no initialize handshake). Acceptance: bilingual_negotiation now round-trips tools/list against a mock 2026 server (the request matcher requires the 2026 _meta to be present) and against a 2025 server — proving one client speaks both specs operationally, validated against mock peers since no first-party 2026 server exists yet. 140 client tests pass; clippy -D warnings clean on bilingual / 2025-only / 2026-only.
…eption CLAUDE.md §Protocol Re-export Rule and ADR-001 now document the third exception: the bilingual client links both versioned protocol crates directly (gated by the client-bilingual / client-2025-only / client-2026-only features) and does not route through the turul-mcp-protocol alias.
… a bare year) CLAUDE.md §"Spec-Version Naming" + AGENTS.md §Coding Style: identify an MCP spec by its full YYYY-MM-DD (or YYYY_MM_DD) date, never by year alone — 2025 shipped two specs (2025-06-18 and 2025-11-25), so `v2026` / `client-2026-only` are ambiguous and forbidden. Applies to modules, identifiers, cargo features, types, and prose; the only dateless tokens are deliberately spec-neutral names.
…date naming
Slice 1 of the 2026 sprint — finish bilingual client operation routing.
- Routed through protocol/v2026_07_28 when the connection locks to 2026-07-28
(per-request _meta + 2026 result shape): tools/call, resources/list,
resources/read, resources/templates/list, prompts/list, prompts/get.
- Removed-from-core methods rejected on a 2026 connection (kept on 2025-11-25):
ping and tasks/* (get/list/cancel/result + task-augmented tools/call) now
return an error before sending, via reject_if_2026_07_28().
- Full-date spec naming (per the new governance rule): module v2026 -> v2026_07_28,
features client-{2025,2026}-only -> client-{2025-11-25,2026-07-28}-only,
helpers send_2026_07_28 / reject_if_2026_07_28.
Acceptance: tests/bilingual_2026_operations.rs round-trips all 6 ops against a
mock 2026 server (each request matcher REQUIRES the 2026 _meta) and proves
ping/tasks are rejected on a 2026 connection. 142 client tests pass; clippy
-D warnings clean on bilingual / 2025-11-25-only / 2026-07-28-only.
…arity) Closes the slice-1 residual the lane-1 gap inventory found: list_tools_paginated, list_resources_paginated, list_resource_templates_paginated, and list_prompts_paginated now inject the per-request _meta and parse the 2026 result (full-result parsers that preserve nextCursor) when the connection locks to 2026-07-28. The non-paginated ops were already routed; these public siblings were still sending 2025-shaped requests on a 2026 connection. Acceptance: paginated_list_routes_through_2026_with_meta_and_cursor (matcher requires BOTH the 2026 _meta and the cursor). 143 client tests pass; clippy -D warnings clean on bilingual / 2025-11-25-only / 2026-07-28-only.
…e drift) The lane-1 gap inventory flagged ADR-030 still claiming the client is "unscheduled / single-spec 2025-11-25" while seven commits of bilingual routing have landed. ADR-030 gains an "IMPLEMENTED" revision-log entry that supersedes the stale note, records the as-built v2026_07_28 module layout, and lists the still-pending items (MRTR InputRequiredResult, completion/complete, server elicitation). CHANGELOG [0.4.0] gains the client-bilingual entry and the stale client-2025-only / client-2026-only feature names are corrected to full dates.
…rotocol alias ADR-029 cutover foundation, landed safely with no behavior change. The alias re-export is now feature-selected: protocol-2025-11-25 (default) / protocol-2026-07-28, mutually exclusive via compile_error! (both-on and neither-on both error). Default stays 2025-11-25 so every existing consumer builds unchanged; the 2026 alias path builds in isolation. Both versioned crates are now optional deps; server/client features forward to whichever is active via `dep?/feature`. This is the bisectable first step of the cutover. The default flip to 2026 and gating the server's removed-type imports (SetLevelRequest, RootsListChangedNotification) — which will compile-break the server until done — is the atomic part that follows. Gates: `cargo build --workspace` clean (default); alias builds `--no-default-features --features protocol-2026-07-28`; mutex fires on both/neither; clippy -D warnings clean on both configs; turul-mcp-server builds unchanged.
ADR-029 cutover layer 2 (after the alias scaffolding be27fce). Every alias-dependent framework crate now forwards protocol-2025-11-25 (default) / protocol-2026-07-28 to the alias and its framework deps, mutually exclusive via the alias compile_error! mutex. Default build/clippy/tests unchanged (229 server lib tests pass); spec-stable leaves (session-storage) build under 2026; examples keep the alias default (2025-11-25), unaffected. Two Cargo constraints forced design choices (documented in ADR-029 revision log): - Cargo 1.96 forbids `default-features = false` when overriding a workspace- inherited dep -> internal framework deps are now explicit path+version deps. - `default-features = false` is all-or-nothing, stripping non-protocol defaults (storage in-memory, http-server sse) -> server re-forwards storage in-memory. Remaining (first-party 2026 server): the protocol-2026-07-28 build of builders/server/http-server needs real 2026 type adaptation (results carry resultType + CacheableResult; builders alone has ~40 type-mismatch errors under 2026) + server/discover handler + stateless path + cross-process tests.
…026-07-28 ADR-029 cutover. turul-mcp-builders now builds under both spec feature builds. 2025-only concepts gated behind #[cfg(feature="protocol-2025-11-25")] (removed in 2026): ToolExecution + Tool.execution, SetLevelRequest, InitializedNotification, RootsListChangedNotification + ListRoots/RootsNotification builders, the sampling traits, the logging/setLevel builder, and the 2025-shaped ToolBuilder/completion/ message builders. Adapted for 2026: ToolSchema.properties (HashMap<String,Value>), HasOutputSchema (returns None under 2026 since ToolOutputSchema is a split type), SamplingMessage shape, the schemars helper. Default build/clippy/tests unchanged (76 unit + 15 doc tests pass); the 2026 build is clean (only SEP-2577 deprecation warnings on the deprecated-but-present Roots/ Logging types). Follow-up for full 2026 server functionality: a 2026-native ToolBuilder (2026 wants Value-properties and no execution field).
…026 extension) ADR-029 cutover. In MCP 2026-07-28 tasks moved out of core to the (not-yet-built) turul-mcp-ext-tasks extension, so turul-mcp-task-storage is 2025-only and the server task runtime is gated behind protocol-2025-11-25: - turul-mcp-task-storage: compile_error! under protocol-2026-07-28. - server: task-storage is now an optional dep enabled only by protocol-2025-11-25; the task/ module, builder task methods, server task-augmentation + tasks/* dispatch, and the ToolExecution-based test impls are all gated 2025-only. Default build/clippy/tests unchanged (229 pass). Under 2026 the server now compiles without a task runtime (task-storage compile wall removed); 38 non-task 2026 errors remain in elicitation/logging/roots/sampling/prompts/resources/meta (next slice).
ADR-029 cutover. turul-mcp-server now builds under both spec feature builds. Adapted to 2026 (core methods that exist in 2026): pagination (2026 results carry nextCursor top-level, no _meta envelope), request params read loosely from raw params (2026 typed params require a full _meta), Call/GetPrompt/ReadResource params -> 2026 *RequestParams, wire notifications via the cross-spec JsonRpcNotification builder. Gated 2025-only (removed/deprecated in the 2026 stateless core): logging (setLevel), sampling, elicitation handlers, the roots-list-changed notification, the initialize handshake handler, and the 2025-shaped ToolBuilder re-export. Default build/clippy/tests unchanged (229 pass); 2026 server compiles. Still to wire: the server/discover handler + stateless request path.
… cascade ADR-029 cutover. turul-mcp-aws-lambda now builds (lib) under both spec feature builds. Cargo: turul-mcp-server and turul-mcp-derive were still bare workspace deps leaking protocol-2025-11-25 onto the alias (tripping the mutex under 2026) — converted to path deps with default-features=false and added to both protocol forwarding features, re-forwarding server http/sse. Source: mirrored the server's 2025-only gating for tasks/sampling/logging/elicitation/initialize handlers + capabilities in builder.rs/server.rs. Default build/clippy green; 2026 lib green. All framework libs (builders, derive, http-server, server, lambda) now build under both protocol-2025-11-25 and protocol-2026-07-28.
This branch is the 0.4 release in preparation. 0.4 becomes current only when the
maintainer opens the PR and merges it; until then it is pre-release and the branch
lock stands. Stated in both CLAUDE.md and AGENTS.md, which is the source of truth.
AGENTS.md carried the same nonexistent-branch defect CLAUDE.md had: six references to
`2026-07-28-MCP-Specification`, with the real branch demoted to a "side-branch of" it.
The merge prohibition therefore named a branch that does not exist. Both files now
name feat/turul-mcp-protocol-2026-07-28; zero stale refs remain in either.
Version sweep, dispositioned rather than swept. 50 live current-state pins moved to
0.4 — including scripts/scaffold-mcp-server.sh, which *generates* a Cargo.toml for
users and was emitting turul-mcp-server = "0.3", so every scaffolded project started
on a stale pin. Also skill example headers, two .version() strings, and the storage /
task / lambda reference guides. The plugin's own v0.7.0 changelog claimed this bump
was done "across SKILL.md, examples, and references"; it was not.
Deliberately NOT moved, and now written down as a rule (CLAUDE.md §Version
References): frozen crates at 0.3.47, since-markers ("Since v0.3.27…", "(v0.3+)"),
changelog history, the v0.3.40/41/42 incident citations that §Test Coverage
Discipline rests on, and external pins (futures, tracing-subscriber, async-stream).
A blanket 0.3→0.4 would have falsified all of them.
Two counting errors of my own, both corrected in place. The release checklist said
"46 v0.3 references across 10 SKILL.md files"; the real prose count was 9 across 5,
of which 8 must not move. And my first CLAUDE.md draft repeated a nine-hit figure
from grepping only `v0.3` — searching `= "0.3"` found 50 more, in different files.
The rule now says grep both forms and explains why the prose-only search misleads.
Docs gate passes.
… and code win Records the decision: where this rule's prose appears to forbid something the MCP spec requires, an ADR decided, or the code already does correctly, the rule is what is wrong. Fix the wording, not the system. Order: spec settles what the protocol crate must contain, an ADR settles a decision already taken, the code settles what is true today. Bounded so it cannot be read as licence: it resolves contradictions, and invoking it requires citing the schema type, ADR number or file. A preference is not a conflict. The §Source of Truth block carried the opposite implication — it listed only the two playbooks and said AGENTS.md wins, with the spec, ADRs and code absent entirely. Both playbooks are prose about the system; that ordering now appears where precedence lives, so the header and the purity rule agree. The traits.rs case is kept in the section as the worked example. Read literally, "Forbidden: trait hierarchies" condemned 75–80 traits the schema itself declares, and on 2026-07-31 that reading cost a round trip — escalated as an architectural question when the real defect was one mislabelled doc comment. Also notes that check-protocol-purity.sh is a grep, hence a proxy: if it flags something the schema requires, fix the label or the grep, never delete spec-mandated code. Docs gate passes; purity check still clean.
…ters CLAUDE.md was 783 lines mixing principles, quick reference, and ten self-contained standing rules inline. Extract each rule verbatim into its own file under docs/rules/ (plain kebab-case, matching docs/compliance/) with an index, and shrink CLAUDE.md to principles + a link table + the material that's genuinely quick reference. Repoint the 15 skill/ADR docs that linked directly to now-moved CLAUDE.md anchors at their new docs/rules/ locations.
…validator turul-jwt-validator 0.3.2 is this workspace's jwt.rs extracted to a sibling repo, and has since gained max_age/stale_window/retry/typed-fetch-errors. Keeping both is drift; ADR-025 already set the precedent for this shape. Proposed, not Accepted — jwt.rs still ships. Records the forced constraints (jsonwebtoken 10->11, no upstream Algorithm re-export, no test injection point), the accepted public break to validate()'s error type, a hardening policy with decided defaults, and an 18-step migration plan. Also documents the ADR status vocabulary actually in use and indexes ADR-031, which was missing.
Repo-wide rustfmt drift, accumulated because AGENTS.md lists `cargo fmt --all -- --check` as a pre-PR gate but nothing enforces it — it is absent from scripts/ci-gates.sh and from every workflow. Formatting only; no semantic change.
turul-mcp-oauth stops owning JWT/JWKS validation. jwt.rs is now a re-export of turul-jwt-validator 0.3.2 plus this framework's hardening policy. BREAKING: JwtValidator::validate returns JwtValidationError, not OAuthError. Accepted deliberately — 0.4.0 is unpublished and middleware.rs:102 is the only in-tree caller, which already mapped the error. - workspace jsonwebtoken 10 -> 11 (required by upstream); kept for the Algorithm re-export only, since upstream does not re-export it and the type appears in with_algorithms' public signature - hardened_validator() applies max_age 15m / stale_window 5m / retry 3x100ms, all of which upstream ships disabled, and rejects a non-loopback plaintext jwks_uri. oauth_resource_server and the example both route through it, so the policy has one definition - OAuthError gains #[non_exhaustive] and JwksFetchError carries the upstream discriminant instead of a flattened string - the 7 key-injection tests are replaced by wiremock-backed tests asserting through the middleware; each negative case pins its rejection reason rather than bare is_err() Revert-and-fail: corrupting the served JWKS modulus fails 6 of them (valid/expired/wrong-audience/wrong-issuer/unknown-key/scope). symmetric_alg stays green correctly — HS256 is refused at the allow-list before any key.
…-032) Deleting jwt.rs invalidated line-and-test citations across seven surfaces. Per AGENTS.md, a "Verified by" cell naming a test that no longer exists is a defect, not a stale doc — so this is part of the slice, not follow-up. - docs/compliance/base-protocol.md: 5 rows repointed from the deleted jwt.rs::test_* to the wiremock-backed middleware tests that replaced them - the TLS-on-JWKS row goes Unknown -> Implemented. Verified upstream does NOT close it (jwks_uri is stored unvalidated and GET'd directly), so hardened_validator does, loopback exempt - ADR-021/022 revision logs: JwtValidator is no longer owned here, and ADR-021's never-implemented TLS posture is now real - skills plugin: the shipped API reference claimed the API was stable, which the validate() break makes false; documents the new knobs and hardened_validator - CHANGELOG: both breaking changes recorded explicitly - CLAUDE.md publish order: notes external siblings are not in the sequence and that oauth needs turul-jwt-validator >= 0.3.2 for the rust_crypto feature - lib.rs: "MCP draft, 2026-07-28 era" -> "MCP 2026-07-28" scripts/ci-gates.sh default: ALL GATES PASSED. fmt clean, purity clean.
Internal and sibling turul-* dependency requirements go from x.y.z to x.y across the workspace manifest and all 34 member manifests. Cargo reads both as caret requirements, so the admitted range is unchanged except that turul-jwt-validator "0.3" now also admits 0.3.0/0.3.1 — which lack the rust_crypto feature. Cargo.lock still resolves 0.3.2. ADR-032 moves Proposed -> Accepted: all four of its gate conditions are met (jwt.rs holds no second implementation, no jsonwebtoken implementation use remains, the rewritten tests fail on reverted delegation, docs reconciled).
…ex drift The loopback exemption in require_secure_jwks_uri tested only the host, so ftp://localhost/jwks.json or ws://127.0.0.1/... passed hardening and would have failed later at fetch time with a worse error. The exemption exists for plaintext HTTP against a local authorization server; it is now scheme-scoped, with a test covering ftp/file/ws on loopback. Also: - ADR-032 still carried a parenthetical from an earlier draft saying a hand-built JwtValidator "stays fully configurable", contradicting the section below it that binds manual construction via hardened_validator. - docs/adr/README.md counted Proposed (1) after ADR-032 moved to Accepted.
AGENTS.md listed `cargo fmt --all -- --check` as a pre-PR requirement, but nothing enforced it — absent from ci-gates.sh and from every workflow. That is why 12 files had drifted before the sweep in 42a7127. Added as a standalone `fmt` gate rather than folded into a lane gate: rustfmt ignores cargo features, so it belongs to neither spec lane. Runs first in `all` because it costs ~1s and is the cheapest failure to learn about. Verified both directions: passes clean (exit 0); appending a mis-formatted fn to turul-mcp-oauth/src/error.rs fails it (exit 1) naming the file and line. Probe removed. AGENTS.md now points pre-PR at ci-gates.sh instead of listing the commands it already runs.
The file recorded test_pagination_with_invalid_cursor as failing on the 2025-11-25 lane and "Untriaged". It was in fact triaged and closed on 2026-07-28 in 1ecbbea, and the test it names no longer exists. The resolution is worth recording rather than just deleting the line: the test asserted a silent restart-from-the-beginning fallback for an unissued cursor, but 3ad1111 had made that return -32602, which the schema backs. Code was right, test was wrong, so the test was migrated — it is now test_invalid_cursor_is_rejected_with_invalid_params. Verified green on a clean ./scripts/ci-gates.sh opt-in-2025 run today: 35 PASS / 0 FAIL, clean working tree.
ci.yml:8 claims "The same gates run locally via scripts/ci-gates.sh", but the fmt gate added in 2e91b93 existed only locally — the workflow had no cargo fmt step, so the parity claim was false. Added as its own job: rustfmt ignores cargo features, so it needs no matrix, cache or disk cleanup, and reports a formatting failure in seconds rather than behind a full build. OUTSTANDING.md is folded and deleted, as the file itself instructed ("do not let it become a second, competing status authority alongside the driver doc"). Both its surviving lines are accounted for: - SubscriptionsListenResult graceful-close emission -> spec-compliance.md rows 336/356, which cited it by *line number* and are now self-contained. - The pagination test recorded as failing/"Untriaged" -> verified closed in 1ecbbea (6f72f7f). Every other referencing doc repointed at the substance rather than the file: final-readiness-audit (schema pin -> schema/README.md), draft-migration-audit (x4), manual-verification, release-checklist 4.4 now ticked. CHANGELOG history entries stand as written per the versioning rules; a note in the 0.4.0 section records where the content went so the old pointers stay followable. Also corrected the stale `cargo fmt --check` row in the spec-compliance gate table to the actual gate shape.
ci.yml:8 said "The same gates run locally via scripts/ci-gates.sh" and AGENTS.md:90 said the script "mirrors the CI lanes". Both are false in the same direction: hosted CI is a subset. Measured today, local -> hosted gate invocations: fmt 1 -> 1 parity default-2026 28 -> 22 opt-in-2025 19 -> 3 lambda 3 -> 0 no hosted job (needs cargo-lambda + Zig) examples 5 -> 0 no hosted job (boots real servers; wants DynamoDB) Adding the fmt job in 4136c1c closed one line of that table and left the claim otherwise intact, which is what made it worth measuring. Both files now state the real relationship and the consequence: a green CI run is necessary but not sufficient for release; ./scripts/ci-gates.sh is the release gate. The counts are written down so the next person to claim parity has to close them first. No gates added — that is a CI cost and flakiness decision, not a doc fix.
The opt-in lane was the widest hosted-CI gap. Now 35 cargo invocations both sides, verified by diffing them command-by-command rather than by eye. Three fixes, in descending importance: - **client narrowed lanes were `cargo build`, not `cargo test`.** The local gate carries an explicit comment on why that is worthless here: the two feature sets select different #[cfg] dispatch arms from the bilingual default, so compiling them proves nothing about the code a single-spec consumer runs. Hosted CI had exactly the defect the local gate was fixed for. Now tests. - Five [[test]] targets were missing, directly under a comment promising "Every [[test]] target declared in tests/Cargo.toml must be invoked here". Two of them are examples_guard and reachability_guard — the guards that catch "present in the tree, absent from CI" rot, themselves absent from CI. - The 15 protocol-2025-11-25-pinned examples had no hosted build. They cannot join [default-members] without tripping the spec mutex, so nothing else compiles them. Also corrects the header counts I added in 70778e0. Those compared local `run` lines against hosted `- name:` steps, which is apples to oranges — several hosted steps run a dozen commands under one name. The real numbers are opt-in-2025 35->35 and default-2026 28->19, not 19->3 and 28->22. Noted in the header so the next reader counts invocations, not step names. Every added command already passes: the local gate_opt_in_2025 ran green today (35 PASS / 0 FAIL, clean tree).
Adds the eight local gate_default steps hosted CI was missing, each placed beside its natural sibling rather than appended: - check-protocol-purity.sh, next to the schema-pin integrity check (both are offline shell guards; verified neither touches the network) - four 2026 wire suites: streaming_e2e_2026, list_pagination_2026, tool_icons_2026, resource_mime_type_2026 - port_handoff, the spec-neutral guard for the reserve->bind race that handed two suites the same ephemeral port - docs_consistency, next to the compliance suite whose artifacts it audits - the client unknown-method-404 regression, next to the bilingual client tests All three lanes are now command-for-command identical to scripts/ci-gates.sh: fmt 1/1, default-2026 28/28, opt-in-2025 35/35, verified by diffing the two sources rather than by eye. Only `lambda` and `examples` stay local-only, for toolchain and wall-clock reasons named in the workflow header — so a green CI run is still not sufficient for release. Every added step was run locally first: 8/8 pass, so this should not turn CI red on landing. Note on the counts: an earlier diff of mine flagged check-schema-pin.sh as missing when it was already present at line 104 — the comparison collected only lines beginning with `cargo` and silently dropped shell-script steps. The real gap was eight, not nine.
The commands matched the local gate but the labels carried pre-renumbering codes: -32001/-32004 for the request-metadata header step and -32003 for the MRTR capability gate. Verified against source rather than copied from the local script: ERROR_CODE_HEADER_MISMATCH = -32020 (headers.rs:58), and the suites assert -32020 (mcp_headers_2026) and -32021 (mrtr_2026). A label naming a code the step does not produce sends the next reader hunting for the wrong constant. Also de-duplicates AGENTS.md:90, where two passes over the same bullet left "green CI is necessary but not sufficient" stated twice and the superset framing tangled with the new parity claim.
client_integration_test spawns minimal-server from target/debug. The opt-in-2025 gate never built it — it only passed because gate_default runs first under `ci-gates.sh all` and leaves the binary behind. The hosted job has no such neighbour, so it failed with "Failed to start minimal-server: No such file or directory (os error 2)". Make the dependency explicit in both places so the gate works standalone.
…nothing The SQLite trio was ignored as "In-memory SQLite database setup issues", which was wrong. SqliteConfig::default() sets verify_tables/create_tables false, so with_config skips migrate() and every query hits "no such table: sessions" — file-backed or not. Opt into migrations and use a tempfile; all three now run by default. (:memory: stays unusable here regardless of pool size, so the comment records that rather than inviting a retry.) Deleted three tests with zero assertions, which would have passed trivially if un-ignored: - schema.rs test_schema_for_primitive_types (loop over commented-out asserts) - test_lambda_streaming_handler_execution (body was a TEST STUB println) - test_lambda_post_streamable_http_notifications The Lambda pair encoded a real open question — nothing verifies Lambda SSE, since e2e-lambda-local.sh only ever sends Accept: application/json. That is now tracked as its own task rather than as tests that cannot fail.
…ation for 0.4.0
Interop
- New peer: the reference MCP Python SDK (mcp==2.0.0), cell P2->R. Passed on
its first run: 9 methods + 5 negatives, no initialize, no session header.
Until now "Python interop" meant only FastMCP, a third-party framework.
- interop-go-sdk.sh had silently stopped running: its pin-currency check read
GO_SDK_VERSION above the line assigning it, so under `set -u` the probe
aborted every time while the matrix still recorded "pass". Moved the check
below the assignment; the cell now passes for real.
- Skips exited 0 in the Go and turul-client probes, making an absent peer
indistinguishable from a green cell. Both now exit 77.
- FastMCP pin 4.0.0b1 -> 4.0.0b2. R->P now drives 9 methods; the peer answers
8 (it returns -32601 for completion/complete), recorded as "9 driven,
8 answered" rather than as coverage we do not have.
Toolchain
- Add rust-toolchain.toml pinning stable, which is what every CI job uses.
Without it the local toolchain floated: on nightly 2026-08-07,
clippy::double_must_use fires on async_trait output and failed
ci-gates.sh opt-in-2025 twice while hosted CI stayed green. No code defect.
Client
- Delete turul-mcp-client's PROTOCOL_VERSION const. It was behaviourally
correct — every initialize_session call site is the 2025 lane — but it was a
second definition of a string McpVersion::V2025_11_25 already owns. Its test
asserted the constant's value under a name claiming it checked the init
request; replaced with one that builds the request.
Examples
- Archive session-logging-proof-test to examples/archived/ (workspace-excluded).
It asserted nothing and ended by asking a human to eyeball three terminals;
logging-test-server + logging-test-client prove the same property with
PASS/FAIL counts. Unwired from all seven reference points.
- examples_guard now understands examples/archived/. Negative-tested: it still
fails on a row naming an example in neither location.
- All 48 example .version() strings now derive from env!("CARGO_PKG_VERSION")
instead of 48 hand-maintained copies of a value each manifest already owns.
- ext-tasks-server: add default-run, so the `cargo run -p` form both its README
and src/main.rs document is no longer ambiguous across its two binaries.
Correct -32003 -> -32021 in its README; -32003 is RATE_LIMIT_EXCEEDED.
Docs
- CLAUDE.md's Task Support section advertised a 2025-only mechanism as
lane-agnostic. TaskSupport has 0 occurrences in the 2026 protocol crate and
with_task_storage is cfg-gated, so a task_support tool on the default lane
fails to compile. Marked 2025-11-25-only and pointed at the tasks extension.
- Reconcile the release checklist against the code: six items read as open but
are done, one (readme = "README.md", 0 of 18) genuinely is not. Correct the
stale fixture-modeling figure to 12 of 88 (13.6%) and note why a naive
`grep -c Modeled` returns 80 for both categories.
…he J3/J4 gap The two headline 2026-07-28 features were self-verified only: every check on MRTR and on request-scoped progress had turul code on both ends of the wire. Both are now driven by the reference MCP Python SDK through a logging proxy, with assertions on the captured bytes. Fixture (examples/interop-fixture-server) - `confirm` — an MRTR tool. Leg 1 answers input_required with one elicit request and an opaque requestState; leg 2 validates the elicited content against the same schema, re-derived rather than persisted, which is the stateless property J3 exists to prove. A tampered requestState is rejected. - `count` — emits N progress notifications carrying the client's own token, and reports how many it actually sent rather than how many it attempted. Probe (scripts/interop-python-sdk.sh) - J3a: a client declaring no elicitation capability is refused with -32021 naming the capability it needs. This assertion fired unprompted on the first run against a probe that had simply forgotten to declare it — the gate works. - J3b: a client that does declare it completes the round trip, and the SDK drives the retry itself. The capture shows two consecutive tools/call frames and a final resultType=complete carrying the elicited answer, so a foreign client finished MRTR unaided. - Negatively: no elicitation/create and no elicitation-complete notification anywhere in the capture, as the stateless core requires. - J4: a progressToken request is answered SSE-framed with three notifications/progress, each echoing the token the CLIENT declared — the probe asserts that match, because a token a client cannot correlate to its own request is noise. No token gets plain JSON and zero frames (ADR-006). Absence of J3/J4 evidence FAILS rather than skips. The fixture is ours and always registers both tools, so a missing one is a regression, not an environmental skip — otherwise deleting a fixture tool would turn its journey green by making it vanish. Verified by breaking `count` to emit nothing: the probe exits 1 with "declared progressToken 10 but no notifications/progress were framed". Restored and re-run green. Also: three live -32003 claims in mrtr-elicitation-server (README, main.rs, client.rs) predate the ADR-027 renumbering. The code emits -32021, per mrtr_2026.rs::undeclared_capability_is_rejected_with_32021. -32003 is RATE_LIMIT_EXCEEDED. Historical -32003 narration in the plan docs is correctly labelled as such and left alone.
…tale error codes Subscriptions — the other half of J4 - interop-fixture-server gains `emit_changes`, broadcasting all four notification flavours. Emitting only the watched type would make "filtered correctly" and "emitted nothing else" indistinguishable. - scripts/interop-python-sdk.sh opens a listen stream asking for resourcesListChanged only, then asserts on proxy-captured frames: the stream acknowledges FIRST, delivers only the requested type, and every frame carries a subscriptionId. Two proxy bugs, both of which produced a hang or a misleading failure rather than a clean one — worth recording because the second is the dangerous shape: - The buffering read() blocks forever on a long-lived stream. The listen branch now forwards incrementally under a socket timeout, because a deadline consulted only AFTER a line arrives never fires once the server goes idle. - The listen handler runs on a daemon thread that can still be streaming when the client finishes. Appending its capture entry at the END raced the assertion pass, which reported "no subscriptions/listen reached the server" while the client had demonstrably received notifications. The entry is now registered up-front and its frame list mutated in place. Manifests - readme = "README.md" on the 13 non-frozen crates that ship one. The three frozen crates are excluded: AGENTS.md forbids doc updates there too, not just code. turul-mcp-ext-tasks and turul-mcp-schema-validation have no README, so they get no key. Stale error codes - Three LIVE -32003 claims in mrtr-elicitation-server (README, main.rs, client.rs) predate the ADR-027 renumbering; the code emits -32021 per mrtr_2026.rs::undeclared_capability_is_rejected_with_32021. -32003 is RATE_LIMIT_EXCEEDED. Historical -32003 narration in the plan docs is labelled as historical and left alone. Gaps recorded rather than left silent (tasks #71, #72): no interop probe drives the tasks extension, and turul-mcp-client is never driven at a real 2025-11-25 server — only at wiremock stubs, which cannot disagree with the client that shares their author. Both are now rows in the interop matrix and the compliance scorecard, because an unstated gap reads as coverage.
syn 2.0 -> 3.0, serial_test 3.5 -> 4.0, jsonschema 0.48 -> 0.49 (a 0.x minor is semver-major by cargo's rules), plus hyper 1.10 -> 1.11, http 1.4 -> 1.5 and aws-config 1.9 -> 1.10. Verified rather than assumed, because three of the six are major bumps and syn underpins turul-mcp-derive, which BOTH spec lanes depend on: - resolution checked with `cargo metadata` — the direct syn is 3.0.3 (the 2.0.119 alongside it is transitive, as is hyper 0.14 via the AWS SDK) - default lane (2026-07-28): 29 PASS / 0 FAIL - opt-in lane (2025-11-25): 36 PASS / 0 FAIL, which is the one that matters most here — it carries the derive-macro doctests jsonschema stays on default-features = false, so the remote/local $ref fetching features remain impossible to compile in; the bump does not widen that surface. Cargo.lock is gitignored, so only the manifest is committed — anyone resolving fresh gets these floors, not the exact versions measured above.
Closes the gap recorded as task #72. The 2026-07-28 half of ADR-030's bilingual contract was covered against a real server; the 2025-11-25 half was covered only by wiremock stubs in bilingual_negotiation.rs. A mock cannot disagree with the client, because the same author wrote both — so if turul-mcp-server's 2025 lane and turul-mcp-client's 2025 lane drifted apart, nothing would have noticed. ci-gates.sh only BUILT client-initialise-server and never pointed the client at it. Lives in the integration-tests crate, not the client crate: that crate's turul-mcp-server dep is already pinned to protocol-2025-11-25, while turul-mcp-client arrives bilingual by default. The client crate cannot host this — its own dev-dep on turul-mcp-server takes default features, which is the 2026 lane, and flipping that would break the existing 2026 e2e. Three tests, all on mechanisms 2026-07-28 removed, so this doubles as the regression guard for the opt-in lane: - the bilingual client locks 2025-11-25 off a REAL server's discover 404, and the server mints an Mcp-Session-Id - that session id survives real traffic (tools/list + tools/call) rather than being re-handshaked per request - tools round-trip, asserting the returned payload rather than merely Ok Wired into gate_opt_in_2025 immediately below the build step it supersedes: 37 PASS / 0 FAIL, up from 36. Ephemeral ports, since the suite runs test binaries in parallel and a fixed port makes failures order-dependent. The tests pass, so no drift existed — but the coverage did not exist either, and that was the finding.
The 2026-07-28 spec's §Versioning and Compatibility defines dual-era — one implementation serving modern and legacy clients, permitted to run "concurrently on the same endpoint or process" — and makes it a MAY. Nothing in our docs said which side of that we are on. turul servers are single-era by construction: protocol-2026-07-28 and protocol-2025-11-25 are mutually exclusive Cargo features enforced by a compile_error!, so a binary speaks one spec or the other. Serving both means two instances. The client is the exception and already links both, negotiating per connection. Documented in three places, each for its own reader: - README.md — the lede now says it plainly, plus a §Single-era servers section with the client-era x server-lane outcome table and the live 400/-32020 body a legacy client actually receives. - docs/compliance/base-protocol.md §4 — two rows (the modern-only SHOULD we already satisfy via headerless_initialize_rejection_names_supported_versions, and dual-era as MAY/not-implemented), plus why it is a choice rather than an omission and why it is not a feature flag. - AGENTS.md — so a future agent does not describe turul as supporting both specs at once, and knows to state the asymmetry when lanes come up. The asymmetry is the part worth surfacing: the spec's own matrix rates legacy client -> modern server as FAILS, and legacy clients have no fall-forward mechanism. A 2026-default turul server is therefore unreachable to 2025-era clients. That is conformant, but it is a lane-selection input users should have. Also formats tests/client_real_2025_server_e2e.rs. I committed ae72d0c having run the 2025 gate but not the fmt gate, which is a separate gate — rustfmt wanted list_tools() broken across lines.
… commands
README: move the per-lane Task Support detail out of Quick Start into
§Tasks Architecture, where the surrounding text explains why the lanes differ.
Every publishable crate now ships a README and declares `readme = ...`
(was 13/15). turul-mcp-ext-tasks and turul-mcp-schema-validation had no
README file at all. The ext-tasks one leads with the provenance caveat:
upstream is experimental, publishes only schema/draft/ and cuts no tags,
so the crate pins commit + checksum and says so where a consumer sees it.
Fix 7 doc commands that named things which do not exist: three test files
that are #[path]-included rather than their own --test targets, a
schemars filter pointed at the wrong crate, a nonexistent `integration`
test target and `integration` feature, and a `cd` into a directory that
is the package name rather than the path. Lane and port coherence audited
across ~155 documented commands and found clean.
CLAUDE.md pre-release step 2 told you to edit .version("x.y.z") strings
in examples that do not contain any — all 45 use env!("CARGO_PKG_VERSION")
and all 55 manifests are already 0.4.0. Replaced with what actually moves
the value, plus a guard grep.
Record the unchecked-box audit in the release checklist: 18 of 20 done
and never ticked, 1 resolved here, 1 cosmetic and open. Correct three
stale claims in the manual-verification doc, the load-bearing one being
the A2/D1 interop premise — written when FastMCP 4 beta was the only
peer, now superseded by three stable peers.
Gates: 76 PASS / 0 FAIL, 3617 tests. fmt clean.
…6-08-15 The [0.4.0] section's newest dated entry was 2026-08-02; everything from 757fd66 onward was unrecorded — the reference Python SDK peer, the J3/J4 interop closure, subscriptions/listen from a peer, the toolchain pin, six dependency bumps, the real-2025-server client e2e, the single-era posture, and today's documentation accuracy pass. Header deliberately stays 'Unreleased'. The release date and comparison links are stamped in the merge slice, so this file always describes what is actually on main rather than what is hoped for on a branch.
hortovanyi
added a commit
that referenced
this pull request
Aug 15, 2026
Header moves from Unreleased to a dated release now that #18 has merged, so CHANGELOG.md describes what is actually on main. Adds the [0.4.0] comparison link and repoints [Unreleased] at v0.4.0...HEAD. Release gate at merge: 76 gates, 3617 tests, 0 failures.
hortovanyi
added a commit
that referenced
this pull request
Aug 15, 2026
The lock said "DO NOT merge into `main` without express authority" and "`main` continues to hold 2025-11-25". Both were true until 2026-08-15 and are now false: the branch merged in #18, was tagged v0.4.0 and deleted, and `main` carries 2026-07-28. Left as-is it would have bound future sessions to a branch that no longer exists. Retains the parts that were never branch-specific — no merging/tagging to `main` without the maintainer asking for that action, and the reminder that `cargo publish` is irreversible. Reframes the branch-conditional guidance as lane-conditional, which is what it actually is now: both specs live on `main` and are selected by mutually exclusive Cargo features, not by branch. §"MCP Specification Compliance (2025-11-25 baseline)" now says plainly that it describes the opt-in lane only and does NOT describe the default build — previously it read as the primary baseline, which inverts the truth after this release. Repoints the two live cross-references to the renamed section. The docs/plans/* mentions are dated audit records and are left as history.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adopts MCP 2026-07-28 — now the released current specification — as the default build. 2025-11-25 remains fully supported as an opt-in lane (
--no-default-features --features protocol-2025-11-25).231 commits, 759 files, +76.5K/−56.7K. The CHANGELOG's
[0.4.0]section is the readable map of this diff and is the recommended entry point for review.What changes for a user
The 2026-07-28 core is a stateless rewrite:
initialize/notifications/initializedserver/discover+ per-request_metaMcp-Session-Idpingserver/discoverfor liveness)logging/setLevel_meta.logLevelresources/subscribesubscriptions/listentasks/*(incl.tasks/list)Plus MRTR (SEP-2322) replacing all server-initiated requests, SEP-2243 request-metadata headers, JSON Schema 2020-12, caching headers, RFC 9207/9728 auth hardening, and error-code renumbering (
-32002→-32602; new-32020/-32021/-32022).A server speaks one spec or the other, never both. The two protocol features are mutually exclusive at compile time. The client is the exception — it links both and negotiates per connection (ADR-030). Per the spec's own compatibility matrix a legacy client → modern server fails, so a 2026-default server is unreachable to 2025-era clients; serving both means two instances.
Verification
./scripts/ci-gates.sh all→ 76 gates PASS / 0 FAIL, 3617 tests, run atcace1a1. Hosted CI covers every lane gate;lambdaandexampleshave no hosted job by design (toolchain and wall-clock), so the local full-suite run is the release gate — see the header comment in.github/workflows/ci.yml.Three stable external peers each drive 9 methods + 5 negative paths with no wire disagreement, re-measured 2026-08-08:
The Python cell additionally covers MRTR (including the
-32021capability gate, with the SDK driving the retry itself) and the notification surface (progress token correlation;subscriptions/listenack-first filtered delivery). Neither is self-verified any more.Known gaps — disclosed, not hidden
None is a defect; each is missing measurement, and each is recorded in
docs/compliance/:@modelcontextprotocol/conformance@0.2.0-alpha.11ships 75 scenarios at 2026-07-28 plus 10 tasks scenarios. A first exploratory run needs a fixture server before its numbers mean anything.Post-merge sequence
[0.4.0]with its release date + comparison links onmain(deliberately leftUnreleasedhere so the file always describesmain).v0.4.0on the merged commit.CLAUDE.md, including first publications ofturul-mcp-ext-appsandturul-mcp-ext-tasksat0.1.0.🤖 Generated with Claude Code