koala73/worldmonitor · error · McpProxyUpstreamError

tools/call error: MCP server rejected request

Error message

tools/call error: MCP server rejected request

What it means

The MCP session was initialized successfully, but the tools/call JSON-RPC request returned HTTP 2xx with a JSON-RPC error object instead of a result. mcpCallTool throws McpProxyUpstreamError('tools/call error: MCP server rejected request') to signal the server refused the specific tool invocation.

Solutions

  1. Read the JSON-RPC error code/message from the response — it distinguishes unknown tool, invalid arguments, and permission failures.
  2. Validate toolArgs against the tool's input schema (fetch it via tools/list) before calling, and ensure it is a plain object.
  3. Confirm the tool name matches exactly what tools/list returned (case-sensitive, including namespace prefixes).
  4. Keep the session alive: make the tools/call soon after initialize and use sticky sessions so Mcp-Session-Id stays valid.
  5. Check the credentials/headers passed to the proxy grant execute rights for the requested tool.

Example fix

// before: arguments don't match the tool's input schema
mcpCallTool(url, 'search', { q: 'world' }, headers);
// after: match the declared schema (parameter is 'query', string, required)
mcpCallTool(url, 'search', { query: 'world' }, headers);
Defensive patterns

Strategy: validation

Validate before calling

// before calling, confirm the tool exists and arguments match its schema
const tools = await mcpProxy.tools(serverUrl, headers);
const tool = tools.find(t => t.name === toolName);
if (!tool) throw new Error('Unknown tool: ' + toolName);
const required = tool.inputSchema?.required || [];
for (const key of required) {
  if (!(key in (toolArgs || {}))) throw new Error('Missing required argument: ' + key);
}

Type guard

function isJsonRpcError(msg) {
  return typeof msg === 'object' && msg !== null && 'error' in msg && msg.error !== undefined;
}

Try / catch

try {
  const result = await mcpProxy.result(serverUrl, toolName, toolArgs, headers);
  return result;
} catch (error) {
  if (error instanceof McpProxyUpstreamError && error.message.startsWith('tools/call error')) {
    // JSON-RPC rejection: unknown tool, schema mismatch, or permission — retry once with a fresh session
    return retryToolCallWithFreshSession(serverUrl, toolName, toolArgs, headers);
  }
  throw error;
}

Prevention

When it happens

Trigger: Thrown from mcpCallTool (called by the result handler) when callData = await parseJsonRpcResponse(callResp) contains { error: ... } for the tools/call request (id: 3) — the session and handshake succeeded but the call itself was rejected.

Common situations: Calling a tool name that does not exist on the server (JSON-RPC 'unknown tool' error); passing arguments that fail the tool's input schema validation; the session's Mcp-Session-Id expiring before the call (idle timeout or non-sticky load balancing); the supplied credentials lack permission to execute that tool; sending toolArgs in the wrong shape (not an object).

Related errors


AI-assisted analysis of koala73/worldmonitor@7d06c8633d (2026-09-15). Data as JSON: /api/errors/c298a932b7ad8803. Report an issue: GitHub.

Appendix: source

Thrown at api/mcp-proxy.ts:607

  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);
  if (callData.error) throw new McpProxyUpstreamError('tools/call error: MCP server rejected request');
  return callData.result;
}

// --- SSE transport (HTTP+SSE, older MCP spec) ---
// Servers whose URL path ends with /sse use this protocol:
//   1. Client GETs the SSE URL — server opens a stream and emits an `endpoint` event
//      containing the URL where the client should POST JSON-RPC messages.
//   2. Client POSTs JSON-RPC to that endpoint URL.
//   3. Server sends responses on the same SSE stream as `data:` lines.

function isSseTransport(url) {
  const p = url.pathname;
  return p === '/sse' || p.endsWith('/sse');
}

function makeDeferred() {
  let resolve, reject;
  const promise = new Promise((res, rej) => { resolve = res; reject = rej; });

View on GitHub (pinned to 7d06c8633d)