Skip to content

fix(mcp): migrate to MCP SDK 2.x so 2026-07-28 clients can connect - #2363

Open
Çağdaş Yürekli (cagdasyurekli) wants to merge 8 commits into
microsoft:mainfrom
cagdasyurekli:fix/mcp-sdk-2x-protocol-support
Open

fix(mcp): migrate to MCP SDK 2.x so 2026-07-28 clients can connect#2363
Çağdaş Yürekli (cagdasyurekli) wants to merge 8 commits into
microsoft:mainfrom
cagdasyurekli:fix/mcp-sdk-2x-protocol-support

Conversation

@cagdasyurekli

Copy link
Copy Markdown

Problem

markitdown-mcp pins mcp~=1.8.0. That SDK predates the 2026-07-28 protocol revision, in which a client opens a connection with server/discover rather than the initialize handshake.

A 1.8.x server cannot answer server/discover. The method is not in the SDK's ClientRequest union, so model_validate raises inside BaseSession._receive_loop, which at that version has no guard around the request branch. The ValidationError unwinds the anyio task group and the server process exits without sending a reply:

mcp/shared/session.py", line 331, in _receive_loop
    validated_request = self._receive_request_type.model_validate(
pydantic_core._pydantic_core.ValidationError: 24 validation errors for ClientRequest

Because nothing is written back, the client is left waiting on a peer that has already died. Observed with the Antigravity CLI, which sits at "still connecting" indefinitely:

mcp_manager.go:855] MCP: 1 server(s) still connecting after 30s: markitdown
mcp_manager.go:855] MCP: 1 server(s) still connecting after 3m0s: markitdown

Related client-side reports: google-antigravity/antigravity-cli#657 (which identifies server/discover + the Python SDK ValidationError as the trigger) and google-antigravity/antigravity-cli#701. The unknown-method handling was fixed upstream in the SDK's 2.x line and closed for 1.x as maintenance-only (modelcontextprotocol/python-sdk#3193), so the fix has to happen here.

Change

Move to mcp>=2.1.1,<3.0.0.

The 2.x server decides a connection's protocol era from the client's first frame — an initialize opens a legacy connection, an enveloped request opens a 2026-07-28 one — so a single build serves both. Hosts still sending initialize are unaffected.

  • FastMCPMCPServer (renamed in 2.x).
  • The hand-rolled Starlette app is replaced by the SDK's sse_app() and streamable_http_app(). Their routes are merged into one app so /sse, /messages/ and /mcp keep their current paths, json_response=True and stateless_http=True are preserved, and both sub-app lifespans are chained (this is what starts the Streamable HTTP session manager).
  • requires-python is unchanged: mcp 2.1.1 is also >=3.10.

Tests

Adds tests/test_stdio_protocols.py, covering both handshake eras end to end plus a regression test that an unknown method no longer takes the process down.

$ python -m pytest tests/test_stdio_protocols.py -q
...                                                                      [100%]
3 passed in 2.16s

black (23.7.0, the pinned pre-commit version) reports no changes.

Verified against real hosts

Host Opens with convert_to_markdown
Claude Code initialize works
Codex initialize works
Antigravity CLI (agy 1.1.23) server/discover works — previously hung indefinitely

Streamable HTTP and SSE were re-checked under --http: POST /mcp returns the initialize result and GET /sse returns 200 text/event-stream.

One note for reviewers: on the modern path, closing stdin immediately after writing a request can tear the connection down before an in-flight tool call is answered. Real hosts hold the pipe open, and the test does the same, but it may be worth a look upstream.

markitdown-mcp pins `mcp~=1.8.0`, which predates the 2026-07-28 protocol
revision. Clients implementing that revision open a connection with
`server/discover` instead of the `initialize` handshake, and a 1.8.x server
cannot answer it: the method is absent from the SDK's `ClientRequest` union,
so validation raises inside the session receive loop, the exception unwinds
the anyio task group, and the server process exits without replying. The
client is left waiting on a peer that is already gone.

Moving to `mcp>=2.1.1,<3.0.0` fixes this. The 2.x server detects the era from
the client's first frame and serves either one on the same build, so hosts
still sending `initialize` are unaffected.

- `FastMCP` is `MCPServer` in 2.x.
- The hand-rolled Starlette app is replaced by the SDK's `sse_app()` and
  `streamable_http_app()`, whose routes are merged so /sse, /messages/ and
  /mcp keep their current paths and behavior. Both sub-app lifespans are
  chained, which is what starts the Streamable HTTP session manager.
- Adds stdio tests covering both handshake eras and asserting that an
  unknown method no longer takes the server process down.

Verified against Claude Code, Codex and the Antigravity CLI: the first two
open with `initialize`, the third with `server/discover`, and a
`convert_to_markdown` call now succeeds on all three.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The migration disables plugins, breaks remote HTTP binds, removes a security warning, and leaves the new tests outside CI.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Migrates the MCP server to SDK 2.x for legacy and 2026-07-28 protocol compatibility.

Changes:

  • Replaces FastMCP with MCPServer.
  • Adopts SDK-provided HTTP/SSE applications.
  • Adds end-to-end stdio protocol tests.
File summaries
File Description
pyproject.toml Updates the MCP SDK dependency.
__main__.py Migrates server and transport setup.
test_stdio_protocols.py Tests both protocol eras and unknown methods.
Review details
  • Files reviewed: 3/3 changed files
  • Comments generated: 4
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread packages/markitdown-mcp/src/markitdown_mcp/__main__.py Outdated
Comment thread packages/markitdown-mcp/src/markitdown_mcp/__main__.py Outdated
Comment thread packages/markitdown-mcp/src/markitdown_mcp/__main__.py Outdated
Comment thread packages/markitdown-mcp/tests/test_stdio_protocols.py

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

HTTP coverage, Python-version execution, and response-order assumptions need correction.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 4/4 changed files
  • Comments generated: 3
  • Review effort level: Balanced

Comment thread .github/workflows/tests.yml Outdated
Comment thread packages/markitdown-mcp/tests/test_stdio_protocols.py Outdated
Comment thread packages/markitdown-mcp/src/markitdown_mcp/__main__.py
@afourney

afourney commented Sep 1, 2026

Copy link
Copy Markdown
Member

Çağdaş Yürekli (@cagdasyurekli) thanks for your PR. The original version omitted important features (enabling plugins), as well as the warning when binding to a non-localhost interface. The latter was added vert intentionally after many people were using the MCP insecurely.

I've fixed both issues, and integrated the tests with the CI. Please test this in your environment, and let me know if it's working for you. If everything checks out, then I will merge it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants