MCP support is unusable on a fresh install: mcp>=1.8 resolves to the incompatible 2.x
Summary
pip install 'cptr[mcp]' currently installs mcp 2.0.0, which cptr 0.9.15 cannot import. Every MCP tool server — stdio and HTTP alike — fails to load, and the reason is not visible anywhere in the UI. The extra is unconstrained (mcp>=1.8), so this affects every new install from the moment 2.0.0 was published.
Reproduced on Windows 11 / Python 3.13 / cptr 0.9.15, but the dependency resolution is platform-independent.
Reproduction
pip install "cptr[mcp]" # resolves mcp==2.0.0
cptr run
Add any MCP server in Settings → Tool Servers and click Verify:
POST /api/admin/tools/servers/<id>/verify -> 400
The response body carries a reason, but the UI shows only a generic failure. The server log shows a traceback ending in:
File "cptr/utils/mcp/client.py", line 24, in <module>
from mcp.client.streamable_http import streamablehttp_client
ImportError: cannot import name 'streamablehttp_client'
Root cause
mcp 2.0.0 is a breaking release. Three changes affect cptr/utils/mcp/client.py:
| 1.x |
2.0 |
streamablehttp_client |
renamed to streamable_http_client |
streamablehttp_client(url, headers=...) |
no headers=; takes an httpx2.AsyncClient |
CallToolResult.isError |
is_error (snake_case fields with camel aliases) |
| yields a 3-tuple |
yields a 2-tuple (TransportStreams) |
The rename alone raises ImportError at module import, so MCPClient never loads and stdio servers break too — even though stdio_client and StdioServerParameters are unchanged in 2.0 and would otherwise work.
Requested fix
Pin the extra. In pyproject.toml, both the mcp extra (line 28) and the all group (line 32):
That is the whole fix for the immediate breakage. Supporting 2.x is separate work — the HTTP transport needs an httpx2.AsyncClient to pass headers, and isError handling has to become version-aware.
Consider making mcp a base dependency. MCP tool servers are a headline feature, and the optional-extra split is itself a recurring source of failure reports — a plain pip install cptr silently produces a build where every MCP server fails to load.
Secondary: the failure is invisible
verify_tool_server already returns a useful body:
{"ok": false, "message": "MCP support is not installed. Run: pip install 'cptr[mcp]'"}
but fetchJSON (frontend/src/lib/apis/index.ts:26) only reads detail and error on a non-2xx response, so message is discarded and the toast shows "Bad Request":
throw new ApiError(res.status, data.detail || data.error || data.message || res.statusText);
Without this, users have to read the server log or open DevTools to learn why anything failed — which is how a one-command fix turned into a multi-hour debugging session.
Also worth fixing (Windows stdio config)
-
Quoted command paths. A path pasted from Explorer arrives as "C:\Program Files\...\server.exe". The process is spawned without a shell, so the quotes become part of the filename and the spawn fails with [WinError 5] Access is denied — which looks like a permissions problem and sends people off checking ACLs, AppLocker, and AV. Strip surrounding quotes in connect_stdio.
-
Arguments split on bare whitespace (Admin/ToolServers.svelte:144): formArgs.trim().split(/\s+/) tears any path containing a space into multiple arguments. Needs a quote-aware split, and re-quoting on load so the field round-trips.
Deeper issues in the stdio manager
Not hit by the above, but they will surface in long-running sessions:
-
Cross-task teardown (utils/mcp/client.py:172). The AsyncExitStack owning the SDK's task group is entered in whichever request task first calls get_client, but aclose() runs from a different task (shutdown, or another request). anyio raises RuntimeError: Attempted to exit cancel scope in a different task, which is swallowed — so the documented shutdown sequence never runs and processes accumulate. The docstring names the constraint the design violates.
-
Dead-process detection never fires (utils/mcp/stdio_manager.py:46). The check is client.session is not None, but session is only cleared in disconnect() — never when the child dies. The "reconnecting" branch is unreachable; a crashed server returns errors until restart.
-
No spawn lock. Two concurrent tool calls for the same server_id both miss the cache and both spawn; one is overwritten in _instances and leaks.
-
invalidate_tool_server_cache cleanup is unreliable (utils/tools.py:2857): asyncio.get_event_loop() from a sync function plus fire-and-forget ensure_future, wrapped in a bare except. From a worker thread it silently does nothing.
-
--reload breaks stdio on Windows. uvicorn's loop factory returns SelectorEventLoop on win32 in subprocess/reload mode, and that loop cannot spawn subprocesses (NotImplementedError).
Environment
- cptr 0.9.15
- Python 3.13, Windows 11 (26200)
- mcp 2.0.0 (broken) / mcp 1.29.0 (works)
- uvicorn 0.49.0
With pip install "mcp<2" and the quotes removed from the command path, a local stdio server connects and enumerates its tools normally — the Windows stdio implementation itself is sound.
MCP support is unusable on a fresh install:
mcp>=1.8resolves to the incompatible 2.xSummary
pip install 'cptr[mcp]'currently installs mcp 2.0.0, which cptr 0.9.15 cannot import. Every MCP tool server — stdio and HTTP alike — fails to load, and the reason is not visible anywhere in the UI. The extra is unconstrained (mcp>=1.8), so this affects every new install from the moment 2.0.0 was published.Reproduced on Windows 11 / Python 3.13 / cptr 0.9.15, but the dependency resolution is platform-independent.
Reproduction
Add any MCP server in Settings → Tool Servers and click Verify:
The response body carries a reason, but the UI shows only a generic failure. The server log shows a traceback ending in:
Root cause
mcp 2.0.0 is a breaking release. Three changes affect
cptr/utils/mcp/client.py:streamablehttp_clientstreamable_http_clientstreamablehttp_client(url, headers=...)headers=; takes anhttpx2.AsyncClientCallToolResult.isErroris_error(snake_case fields with camel aliases)TransportStreams)The rename alone raises
ImportErrorat module import, soMCPClientnever loads and stdio servers break too — even thoughstdio_clientandStdioServerParametersare unchanged in 2.0 and would otherwise work.Requested fix
Pin the extra. In
pyproject.toml, both themcpextra (line 28) and theallgroup (line 32):That is the whole fix for the immediate breakage. Supporting 2.x is separate work — the HTTP transport needs an
httpx2.AsyncClientto pass headers, andisErrorhandling has to become version-aware.Consider making
mcpa base dependency. MCP tool servers are a headline feature, and the optional-extra split is itself a recurring source of failure reports — a plainpip install cptrsilently produces a build where every MCP server fails to load.Secondary: the failure is invisible
verify_tool_serveralready returns a useful body:{"ok": false, "message": "MCP support is not installed. Run: pip install 'cptr[mcp]'"}but
fetchJSON(frontend/src/lib/apis/index.ts:26) only readsdetailanderroron a non-2xx response, somessageis discarded and the toast shows "Bad Request":Without this, users have to read the server log or open DevTools to learn why anything failed — which is how a one-command fix turned into a multi-hour debugging session.
Also worth fixing (Windows stdio config)
Quoted command paths. A path pasted from Explorer arrives as
"C:\Program Files\...\server.exe". The process is spawned without a shell, so the quotes become part of the filename and the spawn fails with[WinError 5] Access is denied— which looks like a permissions problem and sends people off checking ACLs, AppLocker, and AV. Strip surrounding quotes inconnect_stdio.Arguments split on bare whitespace (
Admin/ToolServers.svelte:144):formArgs.trim().split(/\s+/)tears any path containing a space into multiple arguments. Needs a quote-aware split, and re-quoting on load so the field round-trips.Deeper issues in the stdio manager
Not hit by the above, but they will surface in long-running sessions:
Cross-task teardown (
utils/mcp/client.py:172). TheAsyncExitStackowning the SDK's task group is entered in whichever request task first callsget_client, butaclose()runs from a different task (shutdown, or another request). anyio raisesRuntimeError: Attempted to exit cancel scope in a different task, which is swallowed — so the documented shutdown sequence never runs and processes accumulate. The docstring names the constraint the design violates.Dead-process detection never fires (
utils/mcp/stdio_manager.py:46). The check isclient.session is not None, butsessionis only cleared indisconnect()— never when the child dies. The "reconnecting" branch is unreachable; a crashed server returns errors until restart.No spawn lock. Two concurrent tool calls for the same
server_idboth miss the cache and both spawn; one is overwritten in_instancesand leaks.invalidate_tool_server_cachecleanup is unreliable (utils/tools.py:2857):asyncio.get_event_loop()from a sync function plus fire-and-forgetensure_future, wrapped in a bareexcept. From a worker thread it silently does nothing.--reloadbreaks stdio on Windows. uvicorn's loop factory returnsSelectorEventLoopon win32 in subprocess/reload mode, and that loop cannot spawn subprocesses (NotImplementedError).Environment
With
pip install "mcp<2"and the quotes removed from the command path, a local stdio server connects and enumerates its tools normally — the Windows stdio implementation itself is sound.