CherryHQ/cherry-studio · error · Error
Invalid JSON tool arguments for ${name}: ${(e as Error).mess
Error message
Invalid JSON tool arguments for ${name}: ${(e as Error).message} What it means
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.
Source
Thrown at src/main/ai/mcp/McpRuntimeService.ts:1226
const callToolFunc = async ({ server, name, args }: RuntimeCallToolArgs) => {
try {
// Inside the try so an already-aborted signal still hits the finally cleanup below.
if (effectiveSignal.aborted) {
throw getAbortReason(effectiveSignal)
}
getServerLogger(server, { tool: name, callId: toolCallId }).debug(`Calling tool`, {
args: redactSensitive(args)
})
if (typeof args === 'string') {
if (args.trim() === '') {
args = {}
} else {
try {
args = JSON.parse(args)
} catch (e) {
// Fail fast instead of forwarding malformed JSON as a raw string — the MCP
// server expects an object/record, so a bare string yields opaque downstream errors.
throw new Error(`Invalid JSON tool arguments for ${name}: ${(e as Error).message}`)
}
}
}
const sourcePolicy = this.getLatestSourcePolicy(server)
if (isMcpToolDisabledBySource(sourcePolicy, { name })) {
throw new Error(`MCP tool is disabled: ${name}`)
}
// Client init (ping probe, transport connect, OAuth) has no unified timeout at this
// layer — release this call's wait on abort instead of blocking until it settles.
// The shared `pendingClients` init keeps running (only this caller's wait is released),
// and both racers are consumed, so the loser's late rejection is never unhandled.
// The listener is removed once the race settles: `once` only cleans up after an
// abort fires, and the composed signal is retained by the long-lived stream signal —
// leaving it installed would accumulate a closure per tool call.
let handleAbort: (() => void) | undefined
const client = await Promise.race([
this.getOrCreateClient(server),
new Promise<never>((_, reject) => {View on GitHub (pinned to 726446b54c)
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
Example fix
// before
const args = 'search for cats' // malformed — not JSON
await runtime.callToolByServer({ server, name: 'search', args })
// after
const args = { query: 'cats' } // pass as object
await runtime.callToolByServer({ server, name: 'search', args }) Defensive patterns
Strategy: validation
Validate before calling
function normalizeToolArgs(args: unknown): Record<string, unknown> {
if (args == null) return {}
if (typeof args === 'object') return args as Record<string, unknown>
if (typeof args === 'string') {
const trimmed = args.trim()
if (trimmed === '') return {}
try {
return JSON.parse(trimmed)
} catch {
throw new Error(`Tool args are not valid JSON: ${trimmed.slice(0, 100)}`)
}
}
return {}
}
// Use before calling
const normalized = normalizeToolArgs(rawArgs)
await runtime.callToolByServer({ server, name, args: normalized }) Type guard
function isToolArgsObject(args: unknown): args is Record<string, unknown> {
return typeof args === 'object' && args !== null && !Array.isArray(args)
} Try / catch
try {
await runtime.callToolByServer({ server, name, args })
} catch (e) {
if (e instanceof Error && e.message.startsWith('Invalid JSON tool arguments')) {
// Fallback: call with empty args or skip the tool
logger.warn(`Malformed tool args for ${name}, using empty args`)
await runtime.callToolByServer({ server, name, args: {} })
return
}
throw e
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- Invalid tool ID format: ${toolId}
- Invalid command: command must be a non-empty string
- Invalid command: command cannot be empty
- Invalid args: must be an array
- Invalid args: argument at index ${index} must be a string
AI-assisted analysis of CherryHQ/cherry-studio@726446b54c (2026-08-12).
Data as JSON: /api/errors/d9550c2457969ff7.
Report an issue: GitHub.