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
- Read the JSON-RPC error code/message from the response — it distinguishes unknown tool, invalid arguments, and permission failures.
- Validate toolArgs against the tool's input schema (fetch it via tools/list) before calling, and ensure it is a plain object.
- Confirm the tool name matches exactly what tools/list returned (case-sensitive, including namespace prefixes).
- Keep the session alive: make the tools/call soon after initialize and use sticky sessions so Mcp-Session-Id stays valid.
- 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
- Fetch tools/list first and validate toolArgs against the tool's inputSchema before invoking.
- Match tool names exactly, including any namespace prefix, as returned by tools/list.
- Issue tools/call promptly after initialize and use sticky sessions so Mcp-Session-Id does not expire.
- Confirm the credentials for the server grant execute permission on the specific tool.
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
- Invalid MCP server response
- Initialize error: MCP server rejected request
- tools/list error: MCP server rejected request
- ${label} HTTP 400
- Could not resolve ${JSON.stringify(echoCountryInput(raw))} t
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)