ruvnet/ruflo · error
MCP server " " returned HTTP
Error message
MCP server "${server.name}" returned HTTP ${httpStatus}: ${httpErr instanceof Error ? httpErr.message : String(httpErr)} What it means
During Streamable HTTP connect, a definitive non-429 4xx/5xx (per shouldSkipSseFallback: any 400–599 except 408) skips the SSE fallback and throws this error (clientPool.ts:164), embedding the HTTP status, the SDK error message, and the original error as cause. The failure is also recorded, triggering the cooldown map for subsequent attempts.
Solutions
- Reproduce outside the app: curl -v -X POST <url> with the same headers to see the raw status
- Fix the URL (usually append /mcp) or the auth headers in MCP_SERVERS
- For 5xx, check upstream server logs/proxy health
- After fixing, call evictFromPool(server) or wait ~5s so the recorded cooldown does not mask the fix
Example fix
# before (config)
MCP_SERVERS=[{"name":"tools","url":"https://mcp.example.com"}] # 404 -> HTTP status error
# after
MCP_SERVERS=[{"name":"tools","url":"https://mcp.example.com/mcp"}] Defensive patterns
Strategy: fallback
Validate before calling
// smoke-test the entry before adding it to MCP_SERVERS
async function probeMcpEntry(url: string, headers: Record<string, string>) {
const res = await fetch(url, { method: "POST", headers });
if (res.status >= 400) throw new Error(`MCP probe failed: HTTP ${res.status}`);
} Type guard
function isMcpHttpStatusError(e: unknown): boolean {
return e instanceof Error && /MCP server ".+" returned HTTP \d{3}/.test(e.message);
} Try / catch
try {
client = await getClient(server);
} catch (e) {
if (isMcpHttpStatusError(e)) {
// definitive upstream rejection: skip this server, keep session alive without its tools
logger.warn({ err: e }, `[mcp] disabling server ${server.name}`);
return null;
}
throw e;
} Prevention
- Validate MCP_SERVERS entries with a curl/fetch probe at deploy time
- Keep auth headers and the /mcp path in the config source-controlled and reviewed
- Treat 401/404 as config bugs — alert, don't retry
When it happens
Trigger: MCP server URL answering 401 (missing/wrong auth headers), 403 (forbidden key), 404 (wrong path — e.g. forgot the /mcp suffix), or 5xx on the initialize POST; thrown on the first connect for that url+headers key.
Common situations: MCP_SERVERS env entry with a typo'd path; token expired or lacking scopes; reverse proxy (nginx/traefik) misrouting /mcp; upstream deployed a breaking change returning 500 on initialize.
Related errors
- HTTP transport requires host and port configuration
- Maximum sessions ( ) reached
- Unknown transport type
- WebSocket transport requires host and port configuration
- WG mesh layer not initialized (set config.wgMesh = true and…
AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18).
Data as JSON: /api/errors/b6270584b4428f5a.
Report an issue: GitHub.
Appendix: source
Thrown at ruflo/src/ruvocal/src/lib/server/mcp/clientPool.ts:164
// 4xx/5xx (except 408): falling back to SSE will hit the same upstream
// and fail the same way. Surface a clean error with rate-limit info.
if (shouldSkipSseFallback(httpStatus)) {
try {
await client.close?.();
} catch {}
recordFailure(key, httpStatus, httpErr);
if (httpStatus === 429) {
const retryAfterMs = extractRetryAfterMs(httpErr);
throw new McpRateLimitedError(
server.name,
429,
retryAfterMs,
httpErr instanceof Error ? httpErr.message : String(httpErr)
);
}
throw new Error(
`MCP server "${server.name}" returned HTTP ${httpStatus}: ` +
(httpErr instanceof Error ? httpErr.message : String(httpErr)),
{ cause: httpErr instanceof Error ? httpErr : undefined }
);
}
// Recoverable failure (network, 408, transport mismatch) — try SSE fallback.
try {
await client.connect(new SSEClientTransport(url, { requestInit }));
} catch (err) {
try {
await client.close?.();
} catch {}
// Combine both errors for the caller. Honor 429 status if either side
// surfaced a rate-limit (the upstream is rate-limiting independent of
// transport).
const sseStatus = statusFromTransportError(err);View on GitHub (pinned to fa13ee4ad6)