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

  1. Read the JSON-RPC error code/message from the upstream response — '-32602' style errors or 'not initialized' point to session/capability problems.
  2. Ensure the Mcp-Session-Id from the initialize response is being replayed on the tools/list POST (deployments behind load balancers need sticky sessions).
  3. Check that the initialize response declared tools capability; servers reject tools/list when tools were not negotiated.
  4. Re-run initialize + notifications/initialized and retry tools/list promptly in case of session idle timeout.
  5. 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

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


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)