CherryHQ/cherry-studio · error · McpError

InvalidParams

InvalidParams

Error message

'query' is required for search_skills

What it means

The skills server's `search_skills` requires a non-empty `query` string. It checks `if (!query)` and throws McpError InvalidParams. As with 354, the outer try/catch converts this into a tool result `{ content, isError: true }` rather than surfacing InvalidParams at the protocol level. The query is then normalized (`-_` collapsed to spaces, trimmed) before marketplace search.

Source

Thrown at src/main/ai/mcp/servers/skills.ts:109

          case 'install_skill':
            return await this.installSkill(args)
          default:
            throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${toolName}`)
        }
      } catch (error) {
        const message = error instanceof Error ? error.message : String(error)
        logger.error(`Tool error: ${toolName}`, { agentId: this.agentId, error: message })
        return {
          content: [{ type: 'text' as const, text: `Error: ${message}` }],
          isError: true
        }
      }
    })
  }

  private async searchSkills(args: Record<string, string | undefined>) {
    const query = args.query
    if (!query) throw new McpError(ErrorCode.InvalidParams, "'query' is required for search_skills")

    const results = await searchSkillMarketplaces(
      query.replace(/[-_]+/g, ' ').trim(),
      (url) => this.fetchMarketplaceJson(url),
      (source, error) => {
        logger.warn('Skill marketplace search source failed', {
          agentId: this.agentId,
          source,
          error: error instanceof Error ? error.message : String(error)
        })
      }
    )

    if (results.length === 0) {
      return { content: [{ type: 'text' as const, text: `No installable skills found for "${query}".` }] }
    }

    const view = results.map((r) => ({

View on GitHub (pinned to 726446b54c)

Solutions

  1. Pass a non-empty `query`: `{ "query": "pdf reader" }`.
  2. Use the exact key `query` (not `q`, `term`, `search`).
  3. Check the returned `isError` flag and message rather than expecting a thrown exception.

Example fix

// before
{ query: "" }
// after
{ query: "pdf reader" }
Defensive patterns

Strategy: validation

Validate before calling

function buildSearchSkillsArgs(raw: unknown): { query: string } {
  if (typeof (raw as any)?.query !== 'string') throw new TypeError("'query' string required")
  const q = (raw as any).query.trim()
  if (q.length === 0) throw new TypeError("'query' must be non-empty")
  return { query: q }
}

Type guard

const isSearchSkillsArgs = (v: unknown): v is { query: string } =>
  typeof v === 'object' && v !== null && typeof (v as any).query === 'string' && (v as any).query.trim().length > 0

Try / catch

const result = await client.callTool({ name: 'search_skills', arguments: buildSearchSkillsArgs(raw) })
if (result.isError) {
  const text = (result.content[0] as any).text
  if (/'query' is required/.test(text)) {/* rebuild with a non-empty query */}
}

Prevention

When it happens

Trigger: Calling `search_skills` with no `query`, an empty string, null, or undefined. The arg type is `Record<string, string | undefined>`, so non-string values are coerced/ignored.

Common situations: The model calls `search_skills` with `{}`; passes a whitespace-only query; omits the field; uses a different key like `q` or `term`.

Related errors


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