Pull requests / #3139

#3139 serve/mcp: a cut-off HTTP answer is an error, not a TypeError on the waiting side

open · @ctr-137 · 0 commentaires · Sur GitHub

Server & APIDocumentationWindows

Description

Issue: none; found in the gap analysis, related to the HTTP-bridge scenario in #3113

## Summary

`HttpTransport._post` caught `urllib.error.HTTPError`, `OSError`, `ValueError` and `McpError`, but not
`http.client.IncompleteRead` (an `HTTPException`). A Streamable HTTP MCP server that closes in the middle of its body
left the slot with `msg = None` and `error = None`; `_wait` then evaluated `"error" in None` and raised a `TypeError`.

Two user-visible consequences:

- **At startup** `McpServer._start` catches only `McpError`, so the `TypeError` escaped and the server stayed on
  `starting` forever - no `Failed` badge, no reason, and its tools simply missing. That is indistinguishable from a
  server that is still loading.
- **During a tool call** the `TypeError` escaped `McpHub.call`, whose contract is "only `McpCancelled` is raised";
  the chat ended without the `error: ...` result the model should have read.

## What changed

- `serve/mcp.py`
  - `http.client.IncompleteRead` becomes `McpError("the server's answer was cut off: ...")`.
  - Any other `http.client.HTTPException` becomes a failed-answer `McpError` instead of escaping the same way.
  - `_wait` refuses a slot that has neither a message nor an error with a clear `McpError` - the `TypeError` cannot
    come back through a future transport path either.
- `serve/mcp_fake_server.py`: `http_server(port, truncate=...)` and `--truncate METHOD` answer that method with a
  `Content-Length` longer than the body, then close. This is the deterministic reproduction of the bug.
- `serve/test_mcp.py`: two new tests (below).
- `docs/DETAILS.md` and the module docstring list "a cut-off HTTP answer" among the cases that become an `error: ...`
  result.

## How this was checked

`python -m unittest serve.test_mcp` - **27 tests, OK**, two of them new:

| test | result |
| --- | --- |
| truncated `initialize` | server status `failed`, error says "cut off", no tools |
| truncated `tools/list` | server status `failed`, error says "cut off", no tools |
| truncated `tools/call` after a good start | `hub.call` returns `ok=False` with `error: ... cut off`, no `TypeError` |

**Regression proof:** with `serve/mcp.py` reverted (the tests and the fake server kept), the two new tests fail:
`AssertionError: 'starting' != 'failed'` and a `TypeError: argument of type 'NoneType' is not iterable` - 2 failures,
1 error. With the fix: 2 passed.

The full `serve` suite: **850 tests; the only error is `test_winjob.TestWinJob`** (a Windows `tasklist` call returning
`None` here), the same one as on unmodified `main` (848 tests). `test_keepawake` is timing-flaky under the full run
and passes alone; neither is related to this change. No new skips.

## What I left alone

- A cut-off answer does **not** mark the transport broken. One truncated response can be a proxy hiccup, and the next
  call may still work; a dead server already fails with the connection error. A 404 with a session id still marks it
  broken, as before.
- The web app's missing `GET /mcp` retry and the 10 s start wait are the other two paths in the same family; they are
  separate PRs.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Sur le site

Liens install, modèles, releases.