Skip to content

Bug: McpAgent forces internal WebSocket upgrade for SSE connections #172

Closed
@kajirita2002

Description

@kajirita2002

Summary

The McpAgent (from @cloudflare/agents/mcp) internally forces a WebSocket connection to its underlying Durable Object (DO) stub, even when the client connects to the Worker using Server-Sent Events (SSE) via the /sse endpoint provided by McpAgent.mount().

This behavior prevents the use of McpAgent in environments without WebSocket support for the DO communication leg and generates unnecessary WebSocket-related errors or logs in the DO, even when the intended client communication method is purely SSE.

Problem Description

When a client initiates an SSE connection:

  1. Client sends an HTTP request to the Worker's /sse endpoint with the Accept: text/event-stream header.
  2. The McpAgent.mount()'s Workspace handler correctly identifies the request as SSE.
  3. However, when the Worker communicates with the Durable Object (doStub.fetch), it always sends a request including an Upgrade: websocket header.
  4. This initiates a WebSocket upgrade attempt to the Durable Object, irrespective of the client's original SSE request.

This results in the following internal communication flow:
Client --- (SSE) ---> Worker --- (WebSocket Upgrade) ---> Durable Object

Problematic Code Location

The issue appears to stem from the McpAgent.mount()'s fetch handler, specifically where it calls doStub.fetch. The code unconditionally adds the Upgrade: websocket header.

See approximately: https://github.com/cloudflare/agents/blob/main/packages/agents/src/mcp/index.ts#L373-L384

// Relevant snippet showing unconditional WebSocket upgrade attempt
const upgradeUrl = new URL(request.url);
upgradeUrl.searchParams.set("sessionId", sessionId);

// This fetch unconditionally attempts a WebSocket upgrade
const response = await doStub.fetch(
  new Request(upgradeUrl, {
    headers: {
      Upgrade: "websocket", // <<< This is added regardless of client type
      // Required by PartyServer
      "x-partykit-room": sessionId,
    },
  })
);

This internal architecture makes WebSocket support mandatory for the Durable Object when using McpAgent, even if the public-facing interface is intended to be SSE only.

Expected Behavior

When a client connects via the /sse endpoint, the communication between the Worker and the Durable Object should ideally not be forced to upgrade to WebSocket. The system should support configurations where the entire path (Client -> Worker -> DO) can operate without requiring an internal WebSocket connection if the client initiated an SSE connection.

Relation to MCP Specification (PR #206)

This mandatory internal WebSocket upgrade seems somewhat contrary to the flexible transport principles discussed in the Model Context Protocol evolution, such as in modelcontextprotocol/modelcontextprotocol#206 ("[RFC] Replace HTTP+SSE with new 'Streamable HTTP' transport"). That discussion highlights the need for transport flexibility and acknowledges limitations of mandating WebSockets universally due to infrastructure or client constraints. While the PR focuses on the client-server specification, the spirit of avoiding unnecessary transport limitations seems applicable here internally as well.

Potential Solutions

  1. Conditional Upgrade: Modify McpAgent.mount() to only add the Upgrade: "websocket" header to the doStub.fetch call if the original client request was a WebSocket upgrade request.
  2. Configuration Option: Introduce an option in McpAgent.mount() (e.g., internalTransport: "sse" | "websocket") allowing developers to specify the preferred Worker-DO communication method.
  3. Separate SSE Handling: Consider a distinct internal pathway or handler within McpAgent specifically for proxying SSE requests to the DO without WebSocket involvement.

Request

  1. Is this forced internal WebSocket upgrade the intended design for McpAgent, even for clients connecting via SSE? If so, what is the rationale?
  2. If this is not intended, could it be treated as a bug?
  3. Are there any existing workarounds to achieve Client -> SSE -> Worker -> (Non-WebSocket) -> DO communication using the current @cloudflare/agents package?

Thank you for considering this issue.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions