Repository navigation
Conversation
The 1.x line is in maintenance mode (security fixes only) and the `<2` pin from Showdown76py#32 was always meant to be temporary. Dependabot's range bump (Showdown76py#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.
Owner
|
Thanks, the port holds up: same behaviour as Two things blocked merging it as is, so I carried it in #48 with your six commits untouched:
This PR will close when #48 merges. Generated by Claude Code |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
The dedicated migration PR promised when #39 was closed.
mcp1.x is in maintenance mode (security fixes only) and the<2pin from #32 was a stopgap: 2.0 renamedmcp.server.fastmcp.FastMCPtomcp.server.mcpserver.MCPServerand changed enough around it that a range bump could not work. This moves the project tomcp>=2.3,<3and ports every SDK touchpoint, with the three traps that a mechanical rename would have shipped:MCPServerconstructor. With no explicittransport_security, the 2.xstreamable_http_app()auto-enables a loopback-only allowlist (127.0.0.1:*,localhost:*), so every request from claude.ai, ChatGPT or Le Chat would have been421 Misdirected Request. The settings are now built inserver.pyand passed to the app factory.AppsClientSessioninjected theio.modelcontextprotocol/uicapability by testingrequest.rooton the outgoingInitializeRequest. 2.x request types are not RootModels any more, so the check would have been False forever: no capability advertised, no panels, no error.ClientSession(extensions=...)is the 2.x way and replaces the subclass.ToolErrorkeeps its message for the model; any other exception is reported as "Error executing tool X" and the text stays in the server log._metric_toolnow converts unexpected exceptions toToolError(str(e)), so "node not found" style messages still reach the client.Changes
pyproject.toml:mcp>=2.3,<3,[cli]extra dropped (it only pulled typer for a CLI the server never uses).httpxstays for Redfish; the SDK'shttpx2is a separate package and coexists.server.py+ the 9 tool modules:MCPServer,Icon(mime_type=),version=from package metadata (2.x defaults to""), module-leveltransport_security._metric_toolkeeps its async wrapper (the SDK only offloads sync functions, so there is no double offload), re-raisesToolError/MCPErroras is, converts the rest, and counts cancelled calls ascancelledrather thanok.__main__.py(one block):mcp.streamable_http_app(transport_security=...), parent lifespan on the publicmcp.session_manager.run()instead of the child router's private lifespan.dashboard/mcp_bridge.py,dashboard/chat.py:streamable_http_clientwith anhttpx2.AsyncClientcarrying the bearer andTimeout(connect=30, read=300);chat.pyreusesopen_sessioninstead of its own copy of the handshake; snake_case reads (is_error,input_schema,mime_type,structured_content). Wire keys sent to the browser and to Gemini are unchanged,bridge.jsandapp.pyuntouched.buildanddockerjobs nowimport beaconmcp.server(and the bridge) from the wheel and inside the image.--helpandvalidate-confignever import the SDK, which is why the 2.0 break only showed up in the test job.docs/updates.mdupgrade note (pip reinstall + restart, Docker pull, rollback),MCPServerwording indocs/dashboard.mdandCLAUDE.md.Known and left alone
beaconmcp.yaml: 4 MiB request body limit,session_idle_timeout1800 s,max_sessions10000. No tool takes more than 4 MiB today; worth a config knob if file transfers ever go through/mcp.traceparentit puts in_metais left as is.proxmox/panel.py,apps/*, theui/initializehandshake from feat: interactive panels via the MCP Apps extension #34) stays hand-rolled; the SDK'sAppsextension class is not used.auth.py:68has a comment that still says "FastMCP". Left alone on purpose: feat(auth): oauth refresh tokens and persisted oauth tokens #40 rewrites that area and I'd rather not create a conflict for one word.docs/superpowers/specs/2026-04-16-beaconmcp-design.mdmentions FastMCP as history; untouched.2025-06-18gets2025-06-18back (tested); 2.x can also serve2026-07-28to clients that ask for it. No behaviour change for the current connectors.Related
<2pin this removes), chore(deps): update mcp requirement from <2,>=1.0 to >=1.0,<3 #39 (dependabot bump, closed in favour of this PR).__main__.pyelsewhere (OAuth flow,auth.py); this PR's__main__.pychange is the 12-line app/lifespan block only, so whichever lands second rebases cleanly.Tests
ruff checkclean, CI on 3.11/3.12/3.13 (the suite was 537 on main; the twoAppsClientSessiontests are replaced by two new ones and the panel/bridge tests were ported, see thetestcommit).tests/test_mcp_http_stack.pydrives the real app_run_httpbuilds, in-process through the parent lifespan: 401 +resource_metadatawithout a bearer,initialize200 withMcp-Session-Id,tools/listwithtype: objecton every schema,tools/call, the/mcp/c/<slug>rewrite, and 421 on a foreign Host.beaconmcp serveon this branch:client_credentialstoken,initialize200 (negotiated2025-06-18),/mcp/c/<slug>200,Host: evil.example.net421, then the dashboard bridge end to end (open_sessionover httpx2 with the ui extension,list_tools,list_resources,call_tool).