Skip to content

SSE fallback incorrectly uses the same URL as Streamable HTTP, causing 400 errors on stateful servers #122

@ahonn

Description

@ahonn

Summary

When StreamableHTTPClientTransport fails for any reason, mcporter falls back to SSEClientTransport using the same /mcp URL. For servers that implement Streamable HTTP with stateful_mode: true, this bare GET request (without a prior POST initialize and Mcp-Session-Id header) returns 400, and mcporter reports a misleading error:

[mcporter] Failed to authorize 'myserver': SSE error: Non-200 status code (400)

This happens even though the OAuth flow completed successfully and the token was saved.

Reproduction

  1. Target a Streamable HTTP MCP server (e.g., one using rmcp with stateful_mode: true)
  2. Run mcporter auth --http-url http://127.0.0.1:35729/mcp --allow-http
  3. Complete OAuth flow successfully
  4. mcporter reports failure with exit code 1, despite token being saved

Root Cause

In src/runtime/transport.ts lines 166-178:

if (primaryError instanceof Error) {
  logger.info(`Falling back to SSE transport for '${activeDefinition.name}': ${primaryError.message}`);
}
const sseTransport = new SSEClientTransport(command.url, {
  ...baseOptions,
});

Two problems:

  1. Unconditional fallback: Any error from StreamableHTTPClientTransport triggers the SSE fallback, including errors that SSE cannot fix (auth errors, server-side rejections after successful auth)
  2. Same URL reuse: Legacy SSE transport typically uses /sse, not /mcp. Sending a bare GET to a Streamable HTTP endpoint violates the MCP 2025-03-26 spec which requires Mcp-Session-Id on GET requests

Suggested Fix

Only fall back to SSE for errors that suggest the server doesn't support Streamable HTTP at all (404, 405). Don't fall back for auth errors or post-auth failures:

catch (primaryError) {
  if (isUnauthorizedError(primaryError)) {
    // ... existing OAuth promotion logic
  }
  if (primaryError instanceof OAuthTimeoutError) {
    throw primaryError;
  }

  // Only fall back to SSE for transport-level incompatibility
  const status = extractStatusCode(primaryError);
  if (status === 400 || status === 401 || status === 403) {
    throw primaryError; // Server understood the request but rejected it — SSE won't help
  }

  logger.info(`Falling back to SSE transport...`);
  // ... SSE fallback
}

Additionally, handleAuth could treat a successfully saved token as success (exit 0) without requiring a full listTools verification, since the verification step is what triggers this fallback chain.

Impact

Affects all users connecting to Streamable HTTP servers with stateful_mode: true. The token is saved successfully but mcporter exits with code 1, making CI/automation scripts think auth failed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions