Adopt the 2026-07-28 stateless MCP revision (mcp 2.0.0) - #30
Conversation
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>
📝 WalkthroughWalkthroughThe MCP integration now uses ChangesMCP protocol support
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
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
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
⛔ Files ignored due to path filters (1)
uv.lockis excluded by!**/*.lock
📒 Files selected for processing (9)
docs/DEPLOYMENT.mddocs/mycelium.mdpyproject.tomlsrc/mycelium/http.pysrc/mycelium/server.pytests/conftest.pytests/test_auth.pytests/test_mcp_protocol_eras.pytests/test_tool_offload.py
| "itsdangerous>=2.2.0", | ||
| "matplotlib>=3.10.9", | ||
| "mcp>=1.27.0", | ||
| "mcp>=2.0.0", |
There was a problem hiding this comment.
📐 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([<>=!~ ]|$)' {} || trueRepository: 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 -250Repository: 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.mdRepository: 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:
- 1: https://github.com/modelcontextprotocol/python-sdk/releases/tag/v2.0.0
- 2: https://newreleases.io/project/github/modelcontextprotocol/python-sdk/release/v2.0.0
- 3: https://pypi.org/project/mcp/2.0.0/
- 4: https://github.com/modelcontextprotocol/python-sdk
- 5: https://github.com/modelcontextprotocol/python-sdk/blob/main/docs/migration.md
- 6: https://github.com/modelcontextProtocol/python-sdk
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.
| 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 |
There was a problem hiding this comment.
🔒 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 thatupsert_entityis 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.
| 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 |
There was a problem hiding this comment.
🔒 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 srcRepository: 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.pyRepository: 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.tomlRepository: 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:
- 1: SEP-2243: Send required Mcp-Method/Mcp-Name HTTP headers modelcontextprotocol/python-sdk#2715
- 2: https://py.sdk.modelcontextprotocol.io/api/mcp/shared/inbound/
- 3: Conformance burn-down: server-side InputRequiredResult, Mcp-Method/Name validation, x-mcp-header filter (14 scenarios → green) modelcontextprotocol/python-sdk#2974
- 4: https://py.sdk.modelcontextprotocol.io/v2/api/mcp/shared/inbound/
- 5: https://py.sdk.modelcontextprotocol.io/troubleshooting/
- 6: https://github.com/modelcontextprotocol/python-sdk/blob/1963af52/src/mcp/client/session.py
🌐 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:
- 1: https://github.com/modelcontextprotocol/python-sdk/blob/1963af52/src/mcp/server/mcpserver/tools/base.py
- 2: SEP-2243: Send required Mcp-Method/Mcp-Name HTTP headers modelcontextprotocol/python-sdk#2715
- 3: Send SEP-2243 Mcp-Method/Mcp-Name headers from the StreamableHTTP client modelcontextprotocol/python-sdk#2730
- 4: https://github.com/modelcontextprotocol/python-sdk/blob/1963af52/docs/whats-new.md
- 5: Add a client extension API modelcontextprotocol/python-sdk#3034
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.
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
initializehandshake, noMcp-Session-Id, every request self-describing.Existing clients are not affected
One
MCPServerserves both revisions from the same/mcpendpoint, picked per request by theMCP-Protocol-Versionheader. 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.pydrives both shapes against the mounted endpoint and asserts a 2026 client's write is readable by a 2025 client.What changed
mcp.server.fastmcp.FastMCP→mcp.server.mcpserver.MCPServer(v1 package removed upstream, no shim)streamable_http_app()session_manager.run()— a mounted sub-app's own lifespan never runstools/listrole filterServerMiddlewarechainThe 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 aListToolsResult.Operational notes
docs/DEPLOYMENT.md.Mcp-Method/Mcp-Namerouting 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:
Mcp-Session-Idis not bound to the principal that created ittools/listadvertises a few tools whose body gate is stricter than their prefix-derived role🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
/mcp.Documentation
Bug Fixes