Skip to content

MCP STDIO support is unusable on a fresh install: mcp>=1.8 resolves to the incompatible 2.x #202

Description

@Classic298

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):

mcp = ["mcp>=1.8,<2"]

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)

  1. 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.

  2. 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:

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. --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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions