Skip to content

Core MCP client silently treats valid SSE responses as empty successes #110

Description

@PionneerZ

The core MCP client advertises text/event-stream support but parses the whole body only with JSON.parse. A valid SSE response fails parsing, becomes null, and callers receive {}. This loses tool results, isError flags, and JSON-RPC errors.

Tested on commit e497e1a01e171f9559f87e0f998318a9efc06609 with Node.js v24.12.0 on Windows, using only an injected local fetch fixture. No production credentials, API requests, or real phone calls were used. I have not established whether the production endpoint currently selects SSE.

Minimal reproduction

Save this as repro-sse.mjs in the repository root and run node repro-sse.mjs:

import assert from 'node:assert/strict';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { listMcpTools } from './packages/core/lib/mcp-client.js';
import { tokenCachePath, writePrivateJson } from './packages/core/lib/cache.js';
import { MCP_PROTOCOL_VERSION } from './packages/core/lib/constants.js';

const config = {
  cacheRoot: fs.mkdtempSync(path.join(os.tmpdir(), 'calle-sse-repro-')),
  serverUrl: 'https://example.invalid/mcp',
  timeoutSeconds: 2,
  minTtlSeconds: 0,
};
writePrivateJson(tokenCachePath(config.cacheRoot, config.serverUrl), {
  token: { access_token: 'test-token-not-a-credential' },
});
const expected = { tools: [{ name: 'echo', inputSchema: { type: 'object' } }] };
function fetchFor(useSse) {
  return async (_url, options) => {
    const req = JSON.parse(options.body);
    assert.equal(options.headers.Accept, 'application/json, text/event-stream');
    if (req.method === 'notifications/initialized') {
      return new Response(null, { status: 202 });
    }
    const initialize = req.method === 'initialize';
    const result = initialize ? {
      protocolVersion: MCP_PROTOCOL_VERSION,
      capabilities: { tools: {} },
      serverInfo: { name: 'fixture', version: '1' },
    } : expected;
    const json = JSON.stringify({ jsonrpc: '2.0', id: req.id, result });
    const sse = useSse && !initialize;
    return new Response(sse ? `event: message\ndata: ${json}\n\n` : json, {
      headers: { 'content-type': sse ? 'text/event-stream' : 'application/json' },
    });
  };
}
const json = await listMcpTools({ config, fetchImpl: fetchFor(false) });
const sse = await listMcpTools({ config, fetchImpl: fetchFor(true) });
console.log({ json, sse });
assert.deepEqual(json, expected); // passes
assert.deepEqual(sse, expected); // fails: actual {}

Actual behavior

Final response JSON SSE
Tool list with one tool One tool returned {} returned
Tool result with isError: true Error flag/content preserved {} returned
JSON-RPC error -32602 Rejects with McpHttpError Resolves with {}

A six-case parity harness reproduced all three comparisons.

Expected behavior

Equivalent JSON and SSE envelopes should produce equivalent results and errors. Malformed or incomplete streams should raise a useful protocol error instead of returning an empty success.

Cause and impact

In packages/core/lib/mcp-client.js, line 46 calls JSON.parse; lines 67–73 swallow the exception. Line 198 advertises SSE. Lines 245 and 277 conceal the missing decoded response with an empty object.

When a server selects SSE, tool discovery can appear empty and downstream code can lose a reported operation failure.

Suggested direction

Decode according to the response media type, parse SSE event framing, match the JSON-RPC response ID, preserve result/error envelopes, and reject malformed or missing expected responses. Keep empty HTTP 202 notification acknowledgements valid. Add JSON/SSE parity tests plus interleaved-notification and truncated-event coverage.

This report and reproduction were prepared with AI coding assistance.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions