koala73/worldmonitor · error · McpProxyUpstreamError
Invalid MCP server response
Error message
Invalid MCP server response
What it means
The MCP server answered the proxy's JSON-RPC request, but its body could not be parsed as a valid JSON-RPC message. parseJsonRpcResponse reads the (size-bounded) response body and hands it to the strict JSON parser; any parse failure that is not a size/depth/timeout condition is wrapped as McpProxyUpstreamError('Invalid MCP server response'). This guards callers against upstreams that return HTML error pages, truncated bodies, or non-JSON-RPC payloads.
Solutions
- Verify the configured MCP server URL is the actual JSON-RPC/MCP endpoint and is reachable without an HTML interstitial (open it and check what it returns for a POST).
- Check whether a proxy/WAF (Cloudflare, corporate proxy) is intercepting upstream responses and returning HTML challenge pages with 200 status; allowlist the upstream.
- Inspect the cause attached to the McpProxyUpstreamError — it carries the underlying parse error identifying the malformed payload.
- Confirm the upstream MCP server is healthy and running a spec-compliant transport; test with a raw curl POST of an initialize payload.
- If bodies are large or deeply nested, confirm the response is under MAX_MCP_PROXY_RESPONSE_BYTES and within JSON depth limits.
Example fix
// before: pointing at a landing page const url = 'https://mcp.example.com/'; // after: point at the MCP JSON-RPC endpoint const url = 'https://mcp.example.com/mcp';
Defensive patterns
Strategy: try-catch
Validate before calling
const u = new URL(serverUrl);
if (u.protocol !== 'https:') throw new Error('MCP server URL must be https');
// a quick probe that the endpoint answers JSON, not HTML:
const probe = await fetch(u, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 0, method: 'ping' }) });
const ct = probe.headers.get('content-type') || '';
if (!ct.includes('json') && !ct.includes('event-stream')) throw new Error('Endpoint did not return JSON: ' + ct); Try / catch
try {
const tools = await mcpProxy.tools(serverUrl);
} catch (error) {
if (error instanceof McpProxyUpstreamError && error.message === 'Invalid MCP server response') {
console.error('Upstream returned a non-JSON-RPC body', { cause: error.cause });
return { tools: [], degraded: true };
}
throw error;
} Prevention
- Always configure the direct JSON-RPC/MCP endpoint URL, not a landing page or docs route.
- Allowlist the upstream in any WAF/CDN so challenge pages never masquerade as 200 JSON responses.
- Smoke-test the server with a raw curl initialize POST before wiring it into the dashboard.
- Monitor the error's cause field to distinguish HTML interstitials from truncated bodies.
When it happens
Trigger: Thrown from parseJsonRpcResponse (called by initData, listData, callData) when the upstream HTTP 2xx body fails strict JSON parsing (parseMcpProxyJson throws), e.g. an HTML error page, an empty body, malformed JSON, or a body exceeding the configured JSON nesting depth when that error is not the dedicated McpProxyJsonDepthError path.
Common situations: Pointing the proxy at a regular web page or an endpoint that answers errors as HTML with status 200; a reverse proxy (Cloudflare, nginx) intercepting the request and returning a challenge/interstitial page; an MCP server that half-closes the connection mid-body leaving truncated JSON; a server that responds 200 with a non-JSONRPC JSON object.
Understand the failure class
Background: "Invalid JSON response" and "Failed to parse response" errors: when an API answers 200 but the body isn't the JSON your library expected — this error's family across 28 libraries.
Related errors
- Initialize error: MCP server rejected request
- tools/list error: MCP server rejected request
- tools/call error: MCP server rejected request
- relay returned ${resp.status}
- HTTP ${resp.status}
AI-assisted analysis of koala73/worldmonitor@7d06c8633d (2026-09-15).
Data as JSON: /api/errors/dd894c97b628e58f.
Report an issue: GitHub.
Appendix: source
Thrown at api/mcp-proxy.ts:556
for (const line of lines) {
if (line.startsWith('data: ')) {
try {
const parsed = parseMcpProxyJson(line.slice(6));
if (parsed.result !== undefined || parsed.error !== undefined) return parsed;
} catch (error) {
if (error instanceof McpProxyJsonDepthError) throw error;
}
}
}
throw new McpProxyUpstreamError('No result found in SSE response');
}
return parseMcpProxyJson(text);
} catch (error) {
if (error instanceof McpProxyUpstreamError
|| error instanceof ResponseBodyTooLargeError
|| error instanceof McpProxyJsonDepthError
|| proxyFailureFor(error).isTimeout) throw error;
throw new McpProxyUpstreamError('Invalid MCP server response', { cause: error });
}
}
async function sendInitialized(serverUrl, headers, sessionId) {
try {
const { response } = await postJson(serverUrl, {
jsonrpc: '2.0',
method: 'notifications/initialized',
params: {},
}, headers, sessionId);
await cancelResponseBody(response);
} catch (error) {
if (error instanceof McpProxySsrfError) throw error;
/* non-fatal */
}
}
async function mcpListTools(serverUrl, customHeaders) {View on GitHub (pinned to 7d06c8633d)