Skip to content

Adopt the 2026-07-28 stateless MCP revision (mcp 2.0.0) - #30

Merged
realDragonium merged 2 commits into
masterfrom
t3code/investigate-stateless-mcp-support
Aug 9, 2026
Merged

realDragonium merged 2 commits into
masterfrom
t3code/investigate-stateless-mcp-support

Conversation

@realDragonium

@realDragonium realDragonium commented Aug 9, 2026 •

Copy link
Copy Markdown
Owner

Upgrades the MCP Python SDK from 1.28.1 to 2.0.0 so Mycelium speaks the 2026-07-28 protocol revision — the stateless one: no initialize handshake, no Mcp-Session-Id, every request self-describing.

Existing clients are not affected

One MCPServer serves both revisions from the same /mcp endpoint, picked per request by the MCP-Protocol-Version header. There is no dual mount, no second URL, and no date by which anyone must migrate — a client moves when it moves. Nothing to reconfigure on any existing connection.

Verified on the wire, not assumed: tests/test_mcp_protocol_eras.py drives both shapes against the mounted endpoint and asserts a 2026 client's write is readable by a 2025 client.

What changed

Area Change
Import mcp.server.fastmcp.FastMCP → mcp.server.mcpserver.MCPServer (v1 package removed upstream, no shim)
Transport config Moved off the constructor onto streamable_http_app()
Lifespan Host app now enters session_manager.run() — a mounted sub-app's own lifespan never runs
tools/list role filter Monkey-patched private handler → the v2 ServerMiddleware chain

The filter rewrite is not optional: the old code reached into mcp._mcp_server.request_handlers, which no longer exists. That is why the upgrade and the rewrite are one commit.

One subtlety worth knowing when reading the middleware: the dispatcher serializes a handler's result to its wire dict before the middleware chain runs, so the filter narrows a dict, not a ListToolsResult.

Operational notes

  • 2026-era requests need no session affinity; legacy clients still do. Documented in docs/DEPLOYMENT.md.
  • The revision adds Mcp-Method / Mcp-Name routing headers that must agree with the body. nginx forwards them untouched so our template is unchanged, but a header-filtering proxy would break modern clients. There's a test pinning the rejection behaviour.

Testing

741 passed (735 before; 6 new), ruff check src/ tests/ clean.

Reviewed by Codex (gpt-5.6) — no critical or high findings. It raised two authorization issues that are pre-existing rather than introduced here (this diff touches only comments in that code); both are fixed in #31, stacked on this branch:

  • legacy Mcp-Session-Id is not bound to the principal that created it
  • tools/list advertises a few tools whose body gate is stricter than their prefix-derived role

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added support for legacy and modern MCP protocol revisions at /mcp.
    • Added session-based and sessionless request handling.
    • Added role-based filtering for available tools.
    • Added validation for conflicting routing headers.
  • Documentation

    • Expanded deployment guidance for MCP sessions, proxy headers, load balancing, and request limits.
    • Updated MCP integration documentation to reflect the current server implementation.
  • Bug Fixes

    • Improved compatibility with newer MCP clients while retaining legacy client support.

realDragonium and others added 2 commits August 9, 2026 15:30
The v1 line is maintenance-only and does not speak the 2026-07-28
protocol revision, which drops the initialize handshake and the
Mcp-Session-Id header so requests stop being pinned to a worker.

One MCPServer serves both revisions from the same /mcp endpoint, chosen
per request by the MCP-Protocol-Version header, so existing clients keep
working with nothing to reconfigure and no deprecation deadline.

The role-based tools/list filter moves off a monkey-patched private
handler onto the v2 ServerMiddleware chain. The dispatcher serializes a
handler's result before the chain runs, so the filter narrows the wire
dict rather than a ListToolsResult.

Transport configuration moves from the constructor to the app builder,
and the host lifespan now starts the session manager itself — a mounted
sub-app's own lifespan never runs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The compatibility guarantee is the reason the upgrade is safe to ship,
so it is asserted against the mounted endpoint rather than assumed: a
modern client lists tools with no handshake and gets no session header,
a legacy client still gets a session and calls tools, a 2026 client's
write is readable by a 2025 client, and the role filter narrows
tools/list on both paths.

Also pins that contradicting routing headers are rejected, since the
deployment notes now tell operators their proxy must pass Mcp-Method
and Mcp-Name through untouched.

These are the only tests that start the session manager — run() is
once-per-instance — so the module shares one client fixture.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 9, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The MCP integration now uses MCPServer middleware, supports legacy session-based and 2026-07-28 stateless clients, and adds authenticated wire-level tests for routing, shared storage, and role-filtered tools.

Changes

MCP protocol support

Layer / File(s) Summary
MCPServer middleware integration
pyproject.toml, src/mycelium/server.py, tests/test_auth.py, tests/test_tool_offload.py
The project requires MCP 2.0.0 or newer. Role-based tools/list filtering now runs through SDK middleware. Related server and test documentation uses MCP terminology.
Streamable HTTP transport lifecycle
src/mycelium/http.py, tests/conftest.py
The HTTP app configures transport security and starts the session manager from the parent lifespan. Legacy sessions and stateless requests use different session-context behavior.
Cross-era protocol validation
tests/test_mcp_protocol_eras.py
Wire-level tests cover JSON and SSE responses, legacy handshakes, stateless requests, shared tool storage, role filtering, and conflicting routing headers.
Protocol deployment documentation
docs/DEPLOYMENT.md, docs/mycelium.md
Documentation describes supported protocol revisions, proxy and load-balancer requirements, request limits, and MCPServer usage.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant MCPHTTPApp
  participant SessionManager
  participant MCPServer
  participant SharedToolStorage
  Client->>MCPHTTPApp: Send legacy or stateless MCP request
  MCPHTTPApp->>SessionManager: Create or reuse legacy session
  MCPHTTPApp->>MCPServer: Dispatch request
  MCPServer->>SharedToolStorage: Execute tool
  SharedToolStorage-->>MCPServer: Return result
  MCPServer-->>Client: Return JSON or SSE response
Loading

Poem

I’m a rabbit with a protocol map,
Sessions hop, while stateless packets tap.
Tools hide from roles that cannot see,
Shared drafts cross eras free.
MCPServer now leads the way!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 73.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: upgrading to MCP 2.0.0 to support the 2026-07-28 stateless MCP revision.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch t3code/investigate-stateless-mcp-support

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pyproject.toml`:
- Line 24: Constrain the mcp dependency in pyproject.toml to the 2.x major
version by adding an upper bound of <3.0.0 to the existing >=2.0.0 requirement.

In `@tests/test_mcp_protocol_eras.py`:
- Around line 250-252: Update the modern reader assertion around _parse(r) to
reject the non-destructive write tool upsert_entity, preferably by enforcing the
complete reader allowlist; apply the identical assertion or allowlist check to
the legacy reader result at tests/test_mcp_protocol_eras.py lines 268-270.
- Around line 273-298: Extend test_routing_headers_must_agree_with_the_body with
a tools/call request whose Mcp-Name header conflicts with params.name, then
assert the response is HTTP 400 and its parsed error code is HEADER_MISMATCH.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: ee9d542f-864b-4af6-8eef-9de089b64469

📥 Commits

Reviewing files that changed from the base of the PR and between ae83f57 and 5119ad5.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (9)
  • docs/DEPLOYMENT.md
  • docs/mycelium.md
  • pyproject.toml
  • src/mycelium/http.py
  • src/mycelium/server.py
  • tests/conftest.py
  • tests/test_auth.py
  • tests/test_mcp_protocol_eras.py
  • tests/test_tool_offload.py

Comment thread pyproject.toml
"itsdangerous>=2.2.0",
"matplotlib>=3.10.9",
"mcp>=1.27.0",
"mcp>=2.0.0",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n '^\s*"mcp' pyproject.toml
fd -HI -t f '^(uv\.lock|poetry\.lock|pdm\.lock|requirements.*\.txt)$' . \
  -x rg -n -i '(^|[-_])mcp([<>=!~ ]|$)' {} || true

Repository: realDragonium/mycelium

Length of output: 181


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- pyproject.toml ---'
cat -n pyproject.toml

printf '%s\n' '--- MCP references ---'
rg -n -i '\bmcp\b|model context protocol|release policy|compatib' \
  -g '!*.lock' -g '!dist' -g '!build' . | head -250

Repository: realDragonium/mycelium

Length of output: 27863


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- dependency and compatibility policy references ---'
rg -n -i 'version|dependency|upgrade|release|support|compatib|mcp' \
  README.md SETUP.md docs pyproject.toml \
  -g '*.md' -g '*.toml' | head -300

printf '%s\n' '--- relevant MCP documentation sections ---'
sed -n '640,660p' docs/mycelium.md
sed -n '218,246p' docs/DEPLOYMENT.md
sed -n '1,70p' README.md

Repository: realDragonium/mycelium

Length of output: 11810


🌐 Web query:

official MCP Python SDK package mcp version 2.0.0 release compatibility versioning policy

💡 Result:

The official MCP Python SDK package (mcp) version 2.0.0 was released on July 28, 2026 [1][2]. This release marks the current stable version of the SDK, which includes a major architectural rework to support the 2026-07-28 Model Context Protocol specification while maintaining backward compatibility for all earlier protocol revisions [1][3][4]. Versioning and Compatibility Policy: - Stable Release: Version 2.0.0 is the current stable release line [3][4]. - Breaking Changes: Version 2.0.0 introduces significant breaking changes compared to v1.x [1][5]. Users not ready to migrate to v2 are advised to pin their requirements with an upper bound (e.g., mcp>=1.28,<2) to continue using the v1.x line [1][4]. - v1.x Maintenance: The v1.x series has been moved to maintenance mode [1][2]. It is available on the v1.x branch and will continue to receive critical bug fixes and security patches [1][4]. - Dependency Requirements: Version 2.0.0 raises the minimum versions for several shared dependencies and introduces new required ones [5]. Detailed information on these changes and migration steps is available in the official SDK migration guide [1][4][5]. - Python Support: Version 2.0.0 requires Python 3.10 or higher [3][6].

Citations:


Constrain the MCP major version.

The official MCP SDK v2 includes breaking changes. mcp>=2.0.0 also permits future MCP 3.x releases. Use mcp>=2.0.0,<3.0.0 unless the release policy supports untested major versions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pyproject.toml` at line 24, Constrain the mcp dependency in pyproject.toml to
the 2.x major version by adding an upper bound of <3.0.0 to the existing >=2.0.0
requirement.

Comment on lines +250 to +252
names = {t["name"] for t in _parse(r)["result"]["tools"]}
assert "list_entities" in names
assert not any(n.startswith(("delete_", "merge_")) for n in names), names

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Test the complete reader tool-access contract.

Both protocol-era tests only reject destructive tools. They do not reject non-destructive write tools such as upsert_entity.

  • tests/test_mcp_protocol_eras.py#L250-L252: assert that upsert_entity is absent from the modern reader result, or apply the complete reader allowlist.
  • tests/test_mcp_protocol_eras.py#L268-L270: apply the same assertion to the legacy reader result.
📍 Affects 1 file
  • tests/test_mcp_protocol_eras.py#L250-L252 (this comment)
  • tests/test_mcp_protocol_eras.py#L268-L270
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_mcp_protocol_eras.py` around lines 250 - 252, Update the modern
reader assertion around _parse(r) to reject the non-destructive write tool
upsert_entity, preferably by enforcing the complete reader allowlist; apply the
identical assertion or allowlist check to the legacy reader result at
tests/test_mcp_protocol_eras.py lines 268-270.

Comment on lines +273 to +298
def test_routing_headers_must_agree_with_the_body(client, bearer):
"""A modern request whose routing headers contradict its body is rejected.

Pinned because the deployment notes tell operators their proxy must pass
`Mcp-Method` / `Mcp-Name` through untouched. If this stopped being enforced,
that advice would silently become optional — and a proxy rewriting one but
not the other would dispatch something the caller didn't ask for.
"""
body = {"_meta": {
PROTOCOL_VERSION_META_KEY: LATEST_PROTOCOL_VERSION,
CLIENT_INFO_META_KEY: {"name": "era-test", "version": "1.0"},
CLIENT_CAPABILITIES_META_KEY: {},
}}
r = client.post(
"/mcp",
headers={
**_ACCEPT,
"Authorization": bearer["admin"],
MCP_PROTOCOL_VERSION_HEADER: LATEST_PROTOCOL_VERSION,
MCP_METHOD_HEADER: "tools/list", # disagrees with the body below
},
json={"jsonrpc": "2.0", "id": 1, "method": "prompts/list", "params": body},
)

assert r.status_code == 400, r.text
assert _parse(r)["error"]["code"] == HEADER_MISMATCH

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 4 'MCP_NAME_HEADER|MCP_METHOD_HEADER|HEADER_MISMATCH|NAME_BEARING_METHODS' \
  tests/test_mcp_protocol_eras.py src

Repository: realDragonium/mycelium

Length of output: 2809


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- inbound protocol definitions and validation references ---'
rg -n -C 8 'NAME_BEARING_METHODS|MCP_NAME_HEADER|HEADER_MISMATCH|MCP_METHOD_HEADER' . \
  -g '*.py' -g '!tests/test_mcp_protocol_eras.py'

printf '%s\n' '--- relevant test structure and name-bearing cases ---'
rg -n -C 8 'tools/call|MCP_NAME_HEADER|NAME_BEARING_METHODS|routing_headers|HEADER_MISMATCH' \
  tests/test_mcp_protocol_eras.py

printf '%s\n' '--- candidate source files ---'
fd -t f -e py . | rg '(^|/)(inbound|mcp_types|protocol|.*mcp.*)\.py$'

Repository: realDragonium/mycelium

Length of output: 223


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- repository file map ---'
git ls-files | sed -n '1,160p'

printf '%s\n' '--- all protocol-symbol references ---'
rg -n -C 6 'NAME_BEARING_METHODS|MCP_NAME_HEADER|HEADER_MISMATCH|MCP_METHOD_HEADER|tools/call|params\.name' . \
  -g '!tests/test_mcp_protocol_eras.py' || true

printf '%s\n' '--- dependency and protocol configuration ---'
rg -n -C 5 'mcp|MCP|protocol' pyproject.toml poetry.lock requirements*.txt setup.cfg setup.py \
  2>/dev/null || true

printf '%s\n' '--- complete relevant test sections ---'
sed -n '1,165p' tests/test_mcp_protocol_eras.py
sed -n '240,315p' tests/test_mcp_protocol_eras.py

Repository: realDragonium/mycelium

Length of output: 14567


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- MCP integration points ---'
rg -n -i -C 8 'mcp|streamable|session|mounted' src/mycelium pyproject.toml tests \
  -g '*.py' -g '*.toml' | sed -n '1,260p'

printf '%s\n' '--- installed package metadata, if available ---'
python3 - <<'PY'
import importlib.util
import sys

for name in ("mcp", "mcp.shared.inbound", "mcp_types"):
    spec = importlib.util.find_spec(name)
    print(name, "=>", None if spec is None else spec.origin)
PY

printf '%s\n' '--- dependency declaration context ---'
sed -n '1,55p' pyproject.toml

Repository: realDragonium/mycelium

Length of output: 17117


🌐 Web query:

mcp Python SDK mcp.shared.inbound NAME_BEARING_METHODS MCP_NAME_HEADER HEADER_MISMATCH

💡 Result:

In the MCP Python SDK, these components relate to the enforcement of standard HTTP headers as defined by SEP-2243 [1]. They ensure that requests sent over HTTP (specifically via Streamable HTTP/SSE) carry metadata in headers that matches the content of the JSON-RPC body [2][3][1]. NAME_BEARING_METHODS: This is a mapping (defined in mcp.shared.inbound) that links specific JSON-RPC method names to the corresponding parameter field in the request body that contains the name or identifier of the target [2]. It is used to ensure the Mcp-Name header correctly reflects the request context [2]. Current mappings include: - tools/call: maps to "name" - prompts/get: maps to "name" - resources/read: maps to "uri" MCP_NAME_HEADER: This is the constant for the "Mcp-Name" HTTP header [2][4]. It is required on POST requests for methods identified in NAME_BEARING_METHODS to allow intermediaries and servers to inspect the request target without parsing the full JSON body [1]. HEADER_MISMATCH: This is a specific MCP error code (specifically -32020) [5]. It is raised when there is a discrepancy between the metadata provided in the HTTP headers (such as Mcp-Method or Mcp-Name) and the actual data contained within the JSON-RPC request body [2][5]. For example, if the Mcp-Name header does not match the value found in the body's parameter (e.g., params.name), the server will reject the request with this error [2]. In practice, the SDK uses these to perform cross-validation: the client adds these headers during the request stamp process [6], and the server validates them during the inbound request classification rung to guarantee that the transport layer and the application layer are synchronized [2][3]. Conforming clients must send these headers, and if a server encounters a mismatch, it indicates a potential issue with the client implementation or an intermediary altering the request [5][1].

Citations:


🌐 Web query:

site:github.com/modelcontextprotocol/python-sdk "NAME_BEARING_METHODS"

💡 Result:

In the context of the Model Context Protocol (MCP) and its associated SDKs, "name-bearing" refers to methods or entities that explicitly require a name identifier for routing, validation, or protocol-level identification [1][2][3]. The concept is most prominent in the implementation of transport headers and tool definitions: 1. Mcp-Name Header (SEP-2243): Per the MCP specification, certain requests (such as tool calls or resource access) require the inclusion of an Mcp-Name HTTP header [2][3]. This header allows intermediaries, gateways, and load balancers to route requests based on the name of the tool, prompt, or resource without needing to parse the full JSON-RPC body [3][4]. For tools and prompts, this is set to the name parameter; for resources, it is typically set to the URI [2][3]. 2. Tool Definition: When creating a tool from a function, the SDK requires a name to identify the tool [1]. If a name is not explicitly provided, the SDK uses the function's own name (fn.name) [1]. Lambda functions, which lack an intrinsic name, must have an explicit name provided to be registered as a valid tool [1]. 3. Extension API: Custom protocol extensions may require a name_param to be declared, which enables the SDK to automatically mirror a specific key from the request parameters into the Mcp-Name header on every transmission [5]. This ensures that vendor-specific or extended methods remain routable according to standard MCP infrastructure rules [5]. In summary, name-bearing methods in this architecture are those that integrate with the protocol's naming and header-based routing requirements, ensuring that the system can identify and route requests efficiently [2][5][3].

Citations:


Test conflicting Mcp-Name headers.

Add a tools/call request whose Mcp-Name differs from params.name. Assert HTTP 400 with HEADER_MISMATCH.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_mcp_protocol_eras.py` around lines 273 - 298, Extend
test_routing_headers_must_agree_with_the_body with a tools/call request whose
Mcp-Name header conflicts with params.name, then assert the response is HTTP 400
and its parsed error code is HEADER_MISMATCH.

@realDragonium
realDragonium merged commit 1ea60fc into master Aug 9, 2026
2 of 3 checks passed
@realDragonium
realDragonium deleted the t3code/investigate-stateless-mcp-support branch September 8, 2026 11:17
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