{"record":{"id":"d9550c2457969ff7","repo":"CherryHQ/cherry-studio","slug":"invalid-json-tool-arguments-for-name-e-as-e","errorCode":null,"errorMessage":"Invalid JSON tool arguments for ${name}: ${(e as Error).message}","messagePattern":"Invalid JSON tool arguments for (.+?): (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/main/ai/mcp/McpRuntimeService.ts","lineNumber":1226,"sourceCode":"    const callToolFunc = async ({ server, name, args }: RuntimeCallToolArgs) => {\n      try {\n        // Inside the try so an already-aborted signal still hits the finally cleanup below.\n        if (effectiveSignal.aborted) {\n          throw getAbortReason(effectiveSignal)\n        }\n        getServerLogger(server, { tool: name, callId: toolCallId }).debug(`Calling tool`, {\n          args: redactSensitive(args)\n        })\n        if (typeof args === 'string') {\n          if (args.trim() === '') {\n            args = {}\n          } else {\n            try {\n              args = JSON.parse(args)\n            } catch (e) {\n              // Fail fast instead of forwarding malformed JSON as a raw string — the MCP\n              // server expects an object/record, so a bare string yields opaque downstream errors.\n              throw new Error(`Invalid JSON tool arguments for ${name}: ${(e as Error).message}`)\n            }\n          }\n        }\n        const sourcePolicy = this.getLatestSourcePolicy(server)\n        if (isMcpToolDisabledBySource(sourcePolicy, { name })) {\n          throw new Error(`MCP tool is disabled: ${name}`)\n        }\n        // Client init (ping probe, transport connect, OAuth) has no unified timeout at this\n        // layer — release this call's wait on abort instead of blocking until it settles.\n        // The shared `pendingClients` init keeps running (only this caller's wait is released),\n        // and both racers are consumed, so the loser's late rejection is never unhandled.\n        // The listener is removed once the race settles: `once` only cleans up after an\n        // abort fires, and the composed signal is retained by the long-lived stream signal —\n        // leaving it installed would accumulate a closure per tool call.\n        let handleAbort: (() => void) | undefined\n        const client = await Promise.race([\n          this.getOrCreateClient(server),\n          new Promise<never>((_, reject) => {","sourceCodeStart":1208,"sourceCodeEnd":1244,"githubUrl":"https://github.com/CherryHQ/cherry-studio/blob/726446b54cd69ffe51a276638672f6d95ca0768c/src/main/ai/mcp/McpRuntimeService.ts#L1208-L1244","documentation":"Thrown when calling an MCP tool whose arguments arrive as a string that fails JSON.parse. The runtime expects tool args to be an object/record; if a string is passed, it attempts to parse it into an object. Empty strings are coerced to {}, but non-empty malformed strings trigger this fail-fast guard to prevent opaque downstream errors from the MCP server.","triggerScenarios":"An LLM generates a tool call with arguments as a raw string that isn't valid JSON (e.g. 'search for cats' instead of '{\"query\": \"cats\"}'), or a programmatic caller passes a non-JSON string. The catch block wraps the parse error with the tool name for debugging.","commonSituations":"LLM model outputs unstructured text as tool arguments instead of JSON; a streaming/parsing layer truncates the JSON mid-string; the caller serializes an object with a custom (non-JSON) method; Unicode encoding issues corrupt the JSON payload.","solutions":["Ensure the LLM prompt instructs the model to output JSON for tool arguments","If passing args programmatically, pass a JavaScript object directly instead of a JSON string","Add a pre-validation step that attempts JSON.parse on string args before calling the runtime, with a fallback to a default object","Check for truncation in the streaming layer if the JSON appears cut off"],"exampleFix":"// before\nconst args = 'search for cats'  // malformed — not JSON\nawait runtime.callToolByServer({ server, name: 'search', args })\n\n// after\nconst args = { query: 'cats' }  // pass as object\nawait runtime.callToolByServer({ server, name: 'search', args })","handlingStrategy":"validation","validationCode":"function normalizeToolArgs(args: unknown): Record<string, unknown> {\n  if (args == null) return {}\n  if (typeof args === 'object') return args as Record<string, unknown>\n  if (typeof args === 'string') {\n    const trimmed = args.trim()\n    if (trimmed === '') return {}\n    try {\n      return JSON.parse(trimmed)\n    } catch {\n      throw new Error(`Tool args are not valid JSON: ${trimmed.slice(0, 100)}`)\n    }\n  }\n  return {}\n}\n\n// Use before calling\nconst normalized = normalizeToolArgs(rawArgs)\nawait runtime.callToolByServer({ server, name, args: normalized })","typeGuard":"function isToolArgsObject(args: unknown): args is Record<string, unknown> {\n  return typeof args === 'object' && args !== null && !Array.isArray(args)\n}","tryCatchPattern":"try {\n  await runtime.callToolByServer({ server, name, args })\n} catch (e) {\n  if (e instanceof Error && e.message.startsWith('Invalid JSON tool arguments')) {\n    // Fallback: call with empty args or skip the tool\n    logger.warn(`Malformed tool args for ${name}, using empty args`)\n    await runtime.callToolByServer({ server, name, args: {} })\n    return\n  }\n  throw e\n}","preventionTips":["Always pass tool arguments as JavaScript objects, not JSON strings","Validate LLM tool-call output with a JSON schema before forwarding to the MCP runtime","If you must handle string args, pre-parse with try-catch and a clear error message"],"tags":["mcp","tool-calling","json-parsing","validation"],"backgroundTag":null,"analyzedSha":"726446b54cd69ffe51a276638672f6d95ca0768c","analyzedAt":"2026-08-12T17:30:37.448Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}