Skip to content

0.4.0 — adopt MCP 2026-07-28 as the default spec (2025-11-25 becomes the opt-in lane) - #18

Merged
hortovanyi merged 233 commits into
mainfrom
feat/turul-mcp-protocol-2026-07-28
Aug 15, 2026
Merged

0.4.0 — adopt MCP 2026-07-28 as the default spec (2025-11-25 becomes the opt-in lane)#18
hortovanyi merged 233 commits into
mainfrom
feat/turul-mcp-protocol-2026-07-28

Conversation

@hortovanyi

Copy link
Copy Markdown
Contributor

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:

Removed Replacement on 2026
initialize / notifications/initialized server/discover + per-request _meta
Protocol sessions / Mcp-Session-Id stateless; server-minted handles as tool args
ping — (use server/discover for liveness)
logging/setLevel per-request _meta.logLevel
GET SSE endpoint / resources/subscribe subscriptions/listen
Core tasks/* (incl. tasks/list) the Tasks extension (SEP-2663)

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 all76 gates PASS / 0 FAIL, 3617 tests, run at cace1a1. Hosted CI covers every lane gate; lambda and examples have 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:

Peer Version Direction
MCP Python SDK (reference) 2.0.0 peer → turul
MCP Go SDK v1.7.0 peer → turul
MCP TypeScript SDK 2.0.0 peer → turul
FastMCP 4.0.0b2 both directions

The Python cell additionally covers MRTR (including the -32021 capability gate, with the SDK driving the retry itself) and the notification surface (progress token correlation; subscriptions/listen ack-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/:

  • Tasks extension has no external driver. No stable SDK implements it client-side — upstream python-sdk's own baseline states "the SDK does not implement the tasks extension yet". Our coverage has turul code on both ends.
  • Lambda SSE streaming is untested.
  • 12 of 88 upstream fixture directories are modeled (13.6%).
  • Upstream's own conformance suite is not wired up. @modelcontextprotocol/conformance@0.2.0-alpha.11 ships 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

  1. Stamp [0.4.0] with its release date + comparison links on main (deliberately left Unreleased here so the file always describes main).
  2. Tag v0.4.0 on the merged commit.
  3. Publish to crates.io in the dependency order recorded in CLAUDE.md, including first publications of turul-mcp-ext-apps and turul-mcp-ext-tasks at 0.1.0.

🤖 Generated with Claude Code

hortovanyi added 30 commits May 24, 2026 18:03
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
hortovanyi merged commit 6f35e04 into main Aug 15, 2026
10 checks passed
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
hortovanyi deleted the feat/turul-mcp-protocol-2026-07-28 branch August 15, 2026 03:51
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant