CherryHQ/cherry-studio · error · Error

Invalid tool ID format: ${toolId}

Error message

Invalid tool ID format: ${toolId}

What it means

Thrown by `callToolById` when the toolId string does not contain the `__` (double underscore) delimiter. The method splits toolId on `__` to extract the serverId (first segment) and toolName (remaining segments joined back). A toolId without `__` yields a single-element array, failing the `parts.length < 2` guard.

Source

Thrown at src/main/ai/mcp/McpRuntimeService.ts:334

      return
    }

    const status: McpRuntimeStatus = {
      state,
      lastCheckedAt: Date.now(),
      ...(lastError !== undefined ? { lastError } : {})
    }
    cacheService.setShared(key, status)
  }

  /**
   * Call a tool by its full ID (serverId__toolName format).
   * Used by Hub server's runtime.
   */
  public async callToolById(toolId: string, params: unknown, callId?: string): Promise<McpCallToolResponse> {
    const parts = toolId.split('__')
    if (parts.length < 2) {
      throw new Error(`Invalid tool ID format: ${toolId}`)
    }

    const serverId = parts[0]
    const toolName = parts.slice(1).join('__')

    const server = mcpServerService.getById(serverId)

    logger.debug(`[callToolById] Calling tool ${toolName} on server ${server.name}`)

    return this.callToolByServer({
      server,
      name: toolName,
      args: params,
      callId
    })
  }

  public getServerKey(server: McpServer): string {

View on GitHub (pinned to 726446b54c)

Solutions

  1. Ensure toolId is formatted as `${serverId}__${toolName}` before calling callToolById
  2. Check the caller (e.g. Hub server's tool routing) to confirm it prepends the serverId with double underscore
  3. If building toolIds dynamically, use a helper: `${server.id}__${toolDef.name}`
  4. Add a debug log before the call to inspect the actual toolId value being passed

Example fix

// before
const result = await runtime.callToolById('search_web', params)

// after
const result = await runtime.callToolById(`${serverId}__search_web`, params)
Defensive patterns

Strategy: validation

Validate before calling

function buildToolId(serverId: string, toolName: string): string {
  if (!serverId || !toolName) {
    throw new Error('Both serverId and toolName are required')
  }
  return `${serverId}__${toolName}`
}

// Validate before calling
function isValidToolId(toolId: string): boolean {
  return toolId.split('__').length >= 2
}

Type guard

function isFullToolId(toolId: string): boolean {
  const parts = toolId.split('__')
  return parts.length >= 2 && parts[0].length > 0 && parts[1].length > 0
}

Try / catch

try {
  await runtime.callToolById(toolId, params)
} catch (e) {
  if (e instanceof Error && e.message.startsWith('Invalid tool ID format')) {
    // Log the malformed toolId and skip or construct a valid one
    logger.warn(`Skipping tool call with invalid ID: ${toolId}`)
    return null
  }
  throw e
}

Prevention

When it happens

Trigger: Calling `callToolById(toolId, params, callId)` with a plain tool name like 'search' instead of the full 'server-id__search' format. Also triggered by a toolId that is empty, null-coerced, or uses a different separator (single underscore, colon, dot).

Common situations: Hub server runtime or LLM tool-call layer passes a bare tool name without the server prefix; a serialization/deserialization step strips or mangles the delimiter; the tool registration uses a different naming convention than expected.

Related errors


AI-assisted analysis of CherryHQ/cherry-studio@726446b54c (2026-08-12). Data as JSON: /api/errors/d18cdced60e5e789. Report an issue: GitHub.