Skip to content

list_mcp_tools hangs when MCP endpoint returns plain-text error as text/event-stream #348

Description

@51acorsi

Describe the Bug

Problem

When an AGW MCP endpoint returns 200 text/event-stream with a plain-text body (e.g. Jwt issuer is not configured) instead of a valid SSE data: event, await session.list_tools()
in list_server_tools() blocks indefinitely.

The MCP SSE parser discards lines without a data: prefix — silently, with no error. The ClientSession then waits forever for a JSON-RPC response that will never arrive. The httpx
timeout does not protect against this because the HTTP response itself completes successfully in ~1s.

Since all fragments run concurrently via asyncio.gather, one hanging fragment blocks the entire list_mcp_tools() call until the outer app-level timeout fires (up to 180s).

Proposed fixes

1. Per-fragment timeout (defensive minimum)

result = await asyncio.wait_for(session.list_tools(), timeout=30.0)

2. Race against stream closure (detects the problem immediately)
done, _ = await asyncio.wait(
    [session.list_tools(), stream_closed_event],
    return_when=asyncio.FIRST_COMPLETED
)
# if stream closed before list_tools() responded → raise immediately

3. Validate SSE body before entering ClientSession
first_chunk = await response.aread(64)
if not first_chunk.startswith((b"data:", b"event:")):
    raise McpError(f"Non-SSE response: {first_chunk.decode(errors='replace')}")

Context

- Reproduced on sap.btpn8n:apiResource:ManagedN8nMcpServer:v1, AGW EU12
- The upstream fix (return 4xx or a proper SSE error event) is tracked separately
- This SDK fix is defensive and needed regardless of the upstream fix

### Steps to Reproduce

### Reproduction

Send `initialize` followed by `tools/list` to any MCP endpoint that returns a plain-text error:

```bash
# initialize — works, returns valid SSE
curl -X POST "<mcp-endpoint>" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}'

# tools/list — returns 200 but body is plain text, not an SSE data: event
curl -X POST "<mcp-endpoint>" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'

Actual tools/list response:
HTTP/2 200
content-type: text/event-stream

<plain text error message>

Expected tools/list response:
HTTP/2 200
content-type: text/event-stream

event: message
data: {"jsonrpc":"2.0","id":1,"result":{"tools":[...]}}

The body lacks the data: prefix required by the SSE spec. The MCP SSE parser silently discards it, leaving ClientSession blocked on a queue that will never receive a message.

Expected Behavior

Exception must be thrown, but it should not wait indefinitely for a response.

Screenshots

No response

Used Versions

  • Python version via python --version: ...
  • SAP Cloud SDK for Python version: ...
  • Framework version (if applicable, e.g., Flask, FastAPI): ...
Installed packages via pip list or uv pip list
Package list here

Code Examples

# Your code here

Stack Trace

No response

Log File

Log file ...

Affected Development Phase

Getting Started

Impact

No Impact

Timeline

No response

Activity

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

Metadata

Metadata

Labels

bugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions