Skip to content

feat: move to the mcp 2.x SDK (MCPServer), carries #44 - #48

Merged
Showdown76py merged 8 commits into
mainfrom
claude/audit-fixes-ycn7ph-mcp2
Oct 11, 2026
Merged

Showdown76py merged 8 commits into
mainfrom
claude/audit-fixes-ycn7ph-mcp2

Conversation

@Showdown76py

Copy link
Copy Markdown
Owner

Carries @Ailcope's #44 onto the current main. The six SDK migration commits are theirs and unchanged; see #44 for the full description of the port.

Before: #44 conflicts with main, and once merged it could not be installed at all. constraints.txt (added by #47) pins mcp==1.30.0, so pip install -e . -c constraints.txt fails with ResolutionImpossible against mcp>=2.3. CI, the Dockerfile, install.sh and the self-update all install that way.

After: the server runs on mcp 2.3.0 with the same behaviour as main, and every install path resolves.

How:

Exercised on a real server

beaconmcp serve from main (mcp 1.30) and from this branch (mcp 2.3), fake Proxmox API behind it, real client and TOTP, driven by an mcp 1.x client (so an older connector against the new server):

Check main this PR
/mcp without bearer 401 + resource_metadata same
initialize, tools 37 tools, all type: object same
proxmox_list_nodes, proxmox_read_file ok same
unknown node isError, "Node 'nope' is not configured..." same message
Host: mcp.example.com (in allowed_hosts) 200, negotiates 2025-06-18 same
Host: evil.example.net / localhost:8000 421 / 421 same
Origin: https://claude.ai / foreign origin 200 / 403 same
dashboard bridge (open_session, list_tools, call_tool) 37 tools, 3 with UI same

Upgrade path: a venv holding main (mcp 1.30) reinstalled from this tree with -c constraints.txt moves to mcp 2.3.0, pip check is clean and beaconmcp check-startup --config beaconmcp.yaml.example passes. That is what the self-update runs before restarting.

Tests: 640 passed under mcp 2.3.0, ruff check clean.

Closes #44.


Generated by Claude Code

Ailcope and others added 8 commits October 11, 2026 11:41
The 1.x line is in maintenance mode (security fixes only) and the `<2`
pin from #32 was always meant to be temporary. Dependabot's range bump
(#39) could not do it alone: 2.0 is a breaking release for this code
base, so the port lands in the dedicated commits that follow.

- `mcp[cli]>=1.0,<2` -> `mcp>=2.3,<3`. The `[cli]` extra is dropped: it
  only pulled typer for the `mcp` command-line tool the server never
  uses. `httpx` stays for bmc/redfish.py; the SDK's `httpx2` arrives as a
  transitive dependency and the two packages coexist.
`mcp.server.fastmcp.FastMCP` is `mcp.server.mcpserver.MCPServer` in 2.x,
and three things around it changed shape. The transport parameters left
the constructor: without an explicit `transport_security` the 2.x app
factory auto-enables a loopback-only allowlist, so every public Host
(claude.ai, ChatGPT, Le Chat) would have been answered 421. Only
`ToolError` keeps its message for the model now; any other exception is
reported as a generic "Error executing tool X" with the text kept
server-side, which would have hidden "node not found" and friends. And
the server version defaults to "" instead of the SDK's.

- The 10 modules import `MCPServer`; `Icon(mime_type=...)`.
- `server.py` exposes a module-level `transport_security`
  (DNS-rebinding protection, `allowed_hosts`, `allowed_origins`) and
  `_run_http` passes it to `mcp.streamable_http_app(...)`. The parent
  Starlette lifespan uses the public `mcp.session_manager.run()` instead
  of reaching into the child app's router.
- `MCPServer("beaconmcp", ..., version=<package version>)`.
- `_metric_tool` keeps its async wrapper (the SDK only offloads sync
  functions, so there is no double offload) and now re-raises
  `ToolError`/`MCPError` as is, converts anything else to
  `ToolError(str(e))`, and counts a cancelled call as `cancelled`
  instead of `ok`. Comment rewritten: the "FastMCP runs sync tools
  inline" justification no longer holds.
`streamablehttp_client` is gone in 2.x: headers and timeouts now live on
an `httpx2.AsyncClient` handed to `streamable_http_client`, which yields
a 2-tuple. `AppsClientSession` had to go too: it tagged the ui extension
onto `InitializeRequest` by testing `request.root`, and 2.x request types
are no longer RootModels, so the check would have been silently False and
the MCP Apps panels would have stopped rendering without any error.
Result and tool fields are snake_case in 2.x; the `getattr(..., "isError")`
reads would have defaulted to False/None and counted errors as ok.

- `open_session` keeps its signature, builds the httpx2 client with the
  bearer and `Timeout(connect=30, read=300)`, and declares the ui
  extension through `ClientSession(extensions=CLIENT_EXTENSIONS)`.
- `chat.py` reuses `open_session` instead of its own copy of the
  handshake, and reads `is_error`, `input_schema`, `mime_type`,
  `structured_content`. Wire keys sent to the browser and to Gemini
  (`isError`, `mimeType`, `structuredContent`) are unchanged.
- `test_mcp_apps_panel.py`: `MCPServer`, `mime_type`, `call_tool` returns
  a `CallToolResult`; every panel tool's `input_schema` must be
  `type: object` (2.x rejects `{}` at serialization).
- `test_dashboard_mcp_apps.py`: the two `AppsClientSession` tests are
  replaced by one that captures the real `initialize` request and checks
  the ui extension is advertised, and one that checks `open_session`
  wires the bearer, the timeouts and the extensions.
- New `test_mcp_http_stack.py`: captures the app `_run_http` hands to
  uvicorn and drives it in-process through the parent lifespan: 401
  with `resource_metadata` without a bearer, initialize 200 with
  `Mcp-Session-Id` (negotiates 2025-06-18 when offered), tools/list with
  object schemas, tools/call, the `/mcp/c/<slug>` rewrite, and 421 on a
  foreign Host. First test to cover auth, slug, lifespan and the
  DNS-rebinding guard together.
- Fakes in `test_dashboard_unit.py` use the snake_case fields.

Tests: 517 passed under mcp 2.3.0, ruff clean.
`beaconmcp --help` and `validate-config` never import the MCP SDK, so a
break like the 2.0 rename could pass the build and docker jobs. Both now
run `import beaconmcp.server` (and the dashboard bridge from the wheel)
with the dummy secrets of the config job; the image gets the example
yaml mounted at the path its `BEACONMCP_CONFIG` points to.
- `docs/updates.md`: how to move an existing install to this release
  (pip reinstall + restart, Docker pull, rollback).
- `docs/dashboard.md`, `CLAUDE.md`: `MCPServer`, `streamable_http_client`,
  no more `<2` pin wording.
# Conflicts:
#	src/beaconmcp/__main__.py
constraints.txt (added on main by the self-update work) still pinned
mcp==1.30.0, so `pip install -e . -c constraints.txt` hit
ResolutionImpossible against mcp>=2.3: CI, the Dockerfile, install.sh and
the self-update would all have failed to install this tree. Regenerated
with the documented recipe; only the SDK set moves (mcp 2.3.0, mcp-types,
httpx2, httpcore2, opentelemetry-api, truststore; typer, shellingham,
pydantic-settings, httpx-sse and annotated-doc drop out).

The upgrade note now uses the constraints file and check-startup.

Co-authored-by: Ailcope <54411234+Ailcope@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lxm6BaCA2kUHH1f6xvh8kS
@Showdown76py Showdown76py added the enhancement New feature or request label Oct 11, 2026 — with Claude
@Showdown76py
Showdown76py merged commit 9b4cce7 into main Oct 11, 2026
6 checks passed
@Showdown76py
Showdown76py deleted the claude/audit-fixes-ycn7ph-mcp2 branch October 11, 2026 19:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants