koala73/worldmonitor · error · McpProxyUpstreamError
tools/list error: MCP server rejected request
Error message
tools/list error: MCP server rejected request
What it means
After a successful MCP initialize handshake, the tools/list JSON-RPC request completed at the HTTP level (2xx) but the server returned a JSON-RPC error object instead of a tool list. mcpListTools throws McpProxyUpstreamError('tools/list error: MCP server rejected request') because the session is up but the server refuses to enumerate tools.
Solutions
- Read the JSON-RPC error code/message from the upstream response — '-32602' style errors or 'not initialized' point to session/capability problems.
- Ensure the Mcp-Session-Id from the initialize response is being replayed on the tools/list POST (deployments behind load balancers need sticky sessions).
- Check that the initialize response declared tools capability; servers reject tools/list when tools were not negotiated.
- Re-run initialize + notifications/initialized and retry tools/list promptly in case of session idle timeout.
- Verify the supplied auth headers grant permission to list tools on this server.
Example fix
// before: load balancer routes tools/list to a different instance, session lost
// nginx: upstream mcp { server a:8080; server b:8080; }
// after: pin the session to one backend
// nginx: upstream mcp { hash $http_mcp_session_id; server a:8080; server b:8080; } Defensive patterns
Strategy: retry
Validate before calling
// ensure a fresh session before listing tools
const session = await initializeSession(serverUrl, headers); // returns { sessionId } or throws
if (!session.sessionId) console.warn('Server did not issue Mcp-Session-Id; tools/list may be rejected'); Type guard
function isJsonRpcError(msg) {
return typeof msg === 'object' && msg !== null && 'error' in msg && msg.error !== undefined;
} Try / catch
let lastError;
for (let attempt = 0; attempt < 2; attempt++) {
try {
return await mcpProxy.tools(serverUrl, headers); // retry re-runs initialize, getting a fresh session
} catch (error) {
lastError = error;
if (error instanceof McpProxyUpstreamError && error.message.startsWith('tools/list error')) continue;
throw error;
}
}
throw lastError; Prevention
- Use sticky sessions (hash on Mcp-Session-Id) when the MCP server is load-balanced across instances.
- Call tools/list immediately after initialize to avoid session idle expiry.
- Verify the server advertises the tools capability in its initialize result.
- Log the JSON-RPC error code to distinguish auth denials from session loss.
When it happens
Trigger: Thrown from mcpListTools (called by the tools handler) when listData = await parseJsonRpcResponse(listResp) contains { error: ... } for the tools/list request (id: 2), despite initialize having succeeded.
Common situations: The server expired or invalidated the Mcp-Session-Id between initialize and tools/list (idle timeout, multi-instance deployment without sticky sessions); the server requires the notifications/initialized round-trip the proxy sends best-effort and rejects tools/list as 'not initialized'; tools capability was not negotiated in initialize; server-side authorization denies tool listing for the supplied credentials.
Related errors
- Invalid MCP server response
- Initialize error: MCP server rejected request
- tools/call error: MCP server rejected request
- ${label} HTTP ${response.status}
- ${label} HTTP 400
AI-assisted analysis of koala73/worldmonitor@7d06c8633d (2026-09-15).
Data as JSON: /api/errors/90bdaecd22915a68.
Report an issue: GitHub.
Appendix: source
Thrown at api/mcp-proxy.ts:588
/* non-fatal */
}
}
async function mcpListTools(serverUrl, customHeaders) {
const { response: initResp, url: sessionUrl, headers } = await postJson(
serverUrl, buildInitPayload(), buildHeaders(customHeaders), null,
);
if (!initResp.ok) throw new McpProxyUpstreamError(`Initialize failed: HTTP ${initResp.status}`);
const sessionId = initResp.headers.get('Mcp-Session-Id') || initResp.headers.get('mcp-session-id');
const initData = await parseJsonRpcResponse(initResp);
if (initData.error) throw new McpProxyUpstreamError('Initialize error: MCP server rejected request');
await sendInitialized(sessionUrl, headers, sessionId);
const { response: listResp } = await postJson(sessionUrl, {
jsonrpc: '2.0', id: 2, method: 'tools/list', params: {},
}, headers, sessionId);
if (!listResp.ok) throw new McpProxyUpstreamError(`tools/list failed: HTTP ${listResp.status}`);
const listData = await parseJsonRpcResponse(listResp);
if (listData.error) throw new McpProxyUpstreamError('tools/list error: MCP server rejected request');
return listData.result?.tools || [];
}
async function mcpCallTool(serverUrl, toolName, toolArgs, customHeaders) {
const { response: initResp, url: sessionUrl, headers } = await postJson(
serverUrl, buildInitPayload(), buildHeaders(customHeaders), null,
);
if (!initResp.ok) throw new McpProxyUpstreamError(`Initialize failed: HTTP ${initResp.status}`);
const sessionId = initResp.headers.get('Mcp-Session-Id') || initResp.headers.get('mcp-session-id');
const initData = await parseJsonRpcResponse(initResp);
if (initData.error) throw new McpProxyUpstreamError('Initialize error: MCP server rejected request');
await sendInitialized(sessionUrl, headers, sessionId);
const { response: callResp } = await postJson(sessionUrl, {
jsonrpc: '2.0', id: 3, method: 'tools/call',
params: { name: toolName, arguments: toolArgs || {} },
}, headers, sessionId);
if (!callResp.ok) throw new McpProxyUpstreamError(`tools/call failed: HTTP ${callResp.status}`);
const callData = await parseJsonRpcResponse(callResp);View on GitHub (pinned to 7d06c8633d)