Claude Query
querySend a coding prompt to Claude CLI, optionally including files, resuming sessions, and capping cost.
Instructions
Execute a prompt via Claude Code CLI with optional file context and session resume. Claude is an AI coding agent that can generate, analyze, refactor, and explain code.
Capabilities: code generation and refactoring, code analysis and explanation, file understanding (text and images), multi-turn conversations via sessionId.
Cost: Default model is Sonnet (~$0.01-0.10/call). Use effort="low" for simple tasks, effort="high" + model="opus" for complex analysis. Set maxBudgetUsd to cap per-call cost (recommended for effort="max" or model="opus").
Tips:
Set workingDirectory to the target repo for project-aware responses.
Break complex tasks into focused prompts rather than one large request.
Resume multi-turn conversations with sessionId from a previous response's metadata.
Include relevant files via the files parameter for targeted context (text files inlined in prompt, images trigger allowed-tools mode).
Use noSessionPersistence=true for stateless one-shot calls.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| prompt | Yes | The prompt to send to Claude | |
| files | No | File paths (text or images) relative to workingDirectory | |
| model | No | Model alias or full Claude model name | |
| sessionId | No | Claude session ID to resume with --resume | |
| resetSession | No | Clear stored session state before execution (use with sessionId to start fresh) | |
| noSessionPersistence | No | Disable session persistence for ephemeral print calls | |
| workingDirectory | No | Working directory for file resolution and CLI execution | |
| timeout | No | Timeout in milliseconds (default: 60000, image queries: 120000) | |
| maxResponseLength | No | Soft limit on response length in words | |
| maxBudgetUsd | No | Maximum cost budget in USD for this call (passed to --max-budget-usd) | |
| effort | No | Effort level: low, medium, high, or max (passed to --effort) |
Implementation Reference
- src/tools/query.ts:42-60 (handler)Main entry point for the query tool handler. Routes to executeTextQuery or executeImageQuery based on whether image files are included.
export async function executeQuery(input: QueryInput): Promise<QueryResult> { const { prompt, files = [], timeout, maxResponseLength, maxBudgetUsd, effort } = input; const model = resolveModel("query", input.model); const cwd = await resolveCwd(input.workingDirectory); if (files.length > MAX_FILES) { throw new Error(`Too many files: ${files.length} (max ${MAX_FILES})`); } const textFiles = files.filter((f) => !isImageFile(f)); const imageFiles = files.filter((f) => isImageFile(f)); if (imageFiles.length > 0) { return executeImageQuery({ prompt, textFiles, imageFiles, model, timeout, cwd, maxResponseLength, sessionId: input.sessionId, noSessionPersistence: input.noSessionPersistence, maxBudgetUsd, effort }); } return executeTextQuery({ prompt, textFiles, model, timeout, cwd, maxResponseLength, sessionId: input.sessionId, noSessionPersistence: input.noSessionPersistence, maxBudgetUsd, effort }); } - src/tools/query.ts:79-129 (handler)Text-only query execution: reads text files, assembles prompt, spawns Claude CLI, parses output, handles timeouts.
async function executeTextQuery(input: BaseQueryInput): Promise<QueryResult> { const { prompt, textFiles, model, timeout, cwd, maxResponseLength, sessionId, noSessionPersistence, maxBudgetUsd, effort } = input; const fileContents = textFiles.length > 0 ? await readFiles(textFiles, cwd) : []; const fullPrompt = appendLengthLimit(assemblePrompt(prompt, fileContents), maxResponseLength); const useStdin = fullPrompt.length > STDIN_THRESHOLD || textFiles.length > 0; const effectiveTimeout = clampTimeout(timeout, 60_000); const args = buildClaudeArgs({ model, fallbackModel: getFallbackModel(), maxBudgetUsd: resolveMaxBudget(maxBudgetUsd), effort: resolveEffort("query", effort), sessionId, noSessionPersistence, prompt: useStdin ? undefined : fullPrompt, }); const result = await spawnClaude({ args, cwd, stdin: useStdin ? fullPrompt : undefined, timeout: effectiveTimeout }); const filesIncluded = fileContents.filter((f) => !f.skipped).map((f) => f.path); const filesSkipped = fileContents.filter((f) => f.skipped).map((f) => `${f.path}: ${f.skipped}`); if (result.timedOut) { return { response: tryParsePartial(result.stdout, result.stderr, effectiveTimeout), model, filesIncluded, filesSkipped, imagesIncluded: [], timedOut: true, resolvedCwd: cwd, }; } const parsed = parseClaudeOutput(result.stdout, result.stderr); checkAndThrow(result, parsed); return { response: parsed.response, model, sessionId: parsed.sessionId, totalCostUsd: parsed.totalCostUsd, usage: parsed.usage, filesIncluded, filesSkipped, imagesIncluded: [], timedOut: false, resolvedCwd: cwd, }; } - src/tools/query.ts:131-211 (handler)Image-aware query execution: validates image files, assembles prompt with image references, spawns Claude CLI with allowedTools=['Read'], handles timeouts.
async function executeImageQuery(input: ImageQueryInput): Promise<QueryResult> { const { prompt, textFiles, imageFiles, model, timeout, cwd, maxResponseLength, sessionId, noSessionPersistence, maxBudgetUsd, effort } = input; const imageResults = await Promise.all( imageFiles.map(async (img) => { try { const resolved = await resolveAndVerify(img, cwd); const size = await checkFileSize(resolved); if (size > MAX_IMAGE_FILE_SIZE) { return { skipped: `${img}: ${(size / 1024).toFixed(0)}KB exceeds ${(MAX_IMAGE_FILE_SIZE / 1024).toFixed(0)}KB limit` }; } return { resolved, original: img }; } catch (err) { return { skipped: `${img}: ${(err as Error).message}` }; } }), ); const validImages = imageResults.filter( (r): r is { resolved: string; original: string } => "resolved" in r, ); const imageNames = validImages.map((r) => r.original); const skippedImages = imageResults .filter((r): r is { skipped: string } => "skipped" in r) .map((r) => r.skipped); const fileContents = textFiles.length > 0 ? await readFiles(textFiles, cwd) : []; const textPart = assemblePrompt(prompt, fileContents); const imagePart = imageNames.map((p) => `Read and analyze the image at: ${p}`).join("\n"); const fullPrompt = appendLengthLimit( imageNames.length > 0 ? `${textPart}\n\n## Image Files\n\n${imagePart}` : textPart, maxResponseLength, ); const effectiveTimeout = clampTimeout(timeout, IMAGE_QUERY_TIMEOUT); const args = buildClaudeArgs({ model, fallbackModel: getFallbackModel(), maxBudgetUsd: resolveMaxBudget(maxBudgetUsd), effort: resolveEffort("query", effort), sessionId, noSessionPersistence, allowedTools: ["Read"], }); const result = await spawnClaude({ args, cwd, stdin: fullPrompt, timeout: effectiveTimeout }); const filesIncluded = fileContents.filter((f) => !f.skipped).map((f) => f.path); const filesSkipped = [ ...fileContents.filter((f) => f.skipped).map((f) => `${f.path}: ${f.skipped}`), ...skippedImages, ]; if (result.timedOut) { return { response: tryParsePartial(result.stdout, result.stderr, effectiveTimeout), model, filesIncluded, filesSkipped, imagesIncluded: imageNames, timedOut: true, resolvedCwd: cwd, }; } const parsed = parseClaudeOutput(result.stdout, result.stderr); checkAndThrow(result, parsed); return { response: parsed.response, model, sessionId: parsed.sessionId, totalCostUsd: parsed.totalCostUsd, usage: parsed.usage, filesIncluded, filesSkipped, imagesIncluded: imageNames, timedOut: false, resolvedCwd: cwd, }; } - src/tools/query.ts:14-25 (schema)Input type definition for the query tool, specifying all accepted parameters.
export interface QueryInput { prompt: string; files?: string[]; model?: string; sessionId?: string; noSessionPersistence?: boolean; workingDirectory?: string; timeout?: number; maxResponseLength?: number; maxBudgetUsd?: number; effort?: string; } - src/index.ts:40-141 (registration)Registration of the 'query' tool with McpServer, including its input schema (Zod definitions), description, annotations, and the handler callback that calls executeQuery and formats the response.
server.registerTool( "query", { title: "Claude Query", description: queryDescription, inputSchema: { prompt: z.string().describe("The prompt to send to Claude"), files: z .array(z.string()) .optional() .describe("File paths (text or images) relative to workingDirectory"), model: z.string().optional().describe("Model alias or full Claude model name"), sessionId: z .string() .optional() .describe("Claude session ID to resume with --resume"), resetSession: z .boolean() .optional() .describe("Clear stored session state before execution (use with sessionId to start fresh)"), noSessionPersistence: z .boolean() .optional() .describe("Disable session persistence for ephemeral print calls"), workingDirectory: z .string() .optional() .describe("Working directory for file resolution and CLI execution"), timeout: z .number() .optional() .describe("Timeout in milliseconds (default: 60000, image queries: 120000)"), maxResponseLength: z .number() .int() .positive() .optional() .describe("Soft limit on response length in words"), maxBudgetUsd: z .number() .positive() .optional() .describe("Maximum cost budget in USD for this call (passed to --max-budget-usd)"), effort: z .string() .optional() .describe("Effort level: low, medium, high, or max (passed to --effort)"), }, annotations: queryAnnotations, }, async (input, extra) => { const start = Date.now(); const heartbeat = maybeStartHeartbeat( extra._meta as { progressToken?: string | number } | undefined, extra.sendNotification as ProgressNotificationSender, ); try { if (input.resetSession && input.sessionId) { sessionStore.delete(input.sessionId); } const result = await executeQuery( input.resetSession ? { ...input, sessionId: undefined } : input, ); const sessionId = result.sessionId ?? (input.resetSession ? undefined : input.sessionId); if (sessionId) { persist(sessionStore, sessionId, result); } const textMeta: string[] = []; if (result.filesIncluded.length > 0) textMeta.push(`Files included: ${result.filesIncluded.join(", ")}`); if (result.imagesIncluded.length > 0) textMeta.push(`Images included: ${result.imagesIncluded.join(", ")}`); if (result.filesSkipped.length > 0) textMeta.push(`Files skipped: ${result.filesSkipped.join(", ")}`); if (result.timedOut) textMeta.push("(timed out)"); const text = textMeta.length > 0 ? `${result.response}\n\n---\n${textMeta.join("\n")}` : result.response; return { content: [{ type: "text" as const, text }], _meta: buildMeta({ durationMs: Date.now() - start, model: result.model, sessionId: result.sessionId, totalCostUsd: result.totalCostUsd, usage: result.usage, timedOut: result.timedOut, }), }; } catch (e) { console.error("[query]", e); return { content: [{ type: "text" as const, text: `Error: ${getErrorMessage(e)}` }], isError: true, _meta: buildMeta({ durationMs: Date.now() - start }), }; } finally { heartbeat.stop(); } }, );