Structured Output
structuredProduce JSON strictly conforming to a JSON Schema from text or files. Ideal for extraction, classification, and parsing tasks requiring validated machine-readable results.
Instructions
Generate JSON conforming to a provided JSON Schema. Uses Claude CLI's native --json-schema flag for validated output (not client-side validation).
Use for: data extraction from text/files, classification, entity parsing, or any task needing machine-parseable output.
Cost: Similar to query (~$0.01-0.10/call). Schema complexity doesn't significantly affect cost.
Tips:
Pass the JSON Schema as a JSON string in the schema parameter.
Schema max size: 20KB. Keep schemas focused for reliable output.
For extraction tasks, include source text via the files parameter or inline in the prompt.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| prompt | Yes | What to generate or extract | |
| schema | Yes | JSON Schema as a JSON string | |
| files | No | Text file paths to include as context | |
| model | No | Model alias or full Claude model name | |
| sessionId | No | Claude session ID to resume with --resume | |
| 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) | |
| maxBudgetUsd | No | Maximum cost budget in USD for this call (passed to --max-budget-usd) |
Implementation Reference
- src/tools/structured.ts:35-141 (handler)Core handler function that executes the structured tool: validates schema, resolves model, reads files, spawns Claude CLI with --json-schema flag, parses structured_output, and returns validated JSON.
export async function executeStructured(input: StructuredInput): Promise<StructuredResult> { const { prompt, files = [], timeout, sessionId, noSessionPersistence, maxBudgetUsd } = input; const model = resolveModel("structured", input.model); const schemaBytes = Buffer.byteLength(input.schema); if (schemaBytes > MAX_SCHEMA_SIZE) { throw new Error(`Schema too large: ${schemaBytes} bytes (max ${MAX_SCHEMA_SIZE})`); } let parsedSchema: object; try { parsedSchema = JSON.parse(input.schema) as object; } catch { throw new Error("Invalid schema: not valid JSON"); } const imageFiles = files.filter((f) => isImageFile(f)); if (imageFiles.length > 0) { throw new Error("Structured tool does not support image files (text only)"); } if (files.length > MAX_FILES) { throw new Error(`Too many files: ${files.length} (max ${MAX_FILES})`); } const cwd = await resolveCwd(input.workingDirectory); const fileContents = files.length > 0 ? await readFiles(files, cwd) : []; const fullPrompt = assemblePrompt(prompt, fileContents); const useStdin = fullPrompt.length > STDIN_THRESHOLD || files.length > 0; const effectiveTimeout = clampTimeout(timeout, 60_000); const args = buildClaudeArgs({ model, fallbackModel: getFallbackModel(), maxBudgetUsd: resolveMaxBudget(maxBudgetUsd), sessionId, noSessionPersistence, jsonSchema: JSON.stringify(parsedSchema), 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: `Structured query timed out after ${effectiveTimeout / 1000}s.`, valid: false, model, filesIncluded, filesSkipped, timedOut: true, }; } const parsed = parseClaudeOutput(result.stdout, result.stderr); checkAndThrow(result, parsed); // Claude CLI places --json-schema output in the structured_output field. // Use key-existence check (not truthy) to handle scalar values like false, 0, "", null. const raw = parsed.raw as Record<string, unknown> | undefined; if (raw && "structured_output" in raw) { return { response: JSON.stringify(raw.structured_output), valid: true, model, sessionId: parsed.sessionId, totalCostUsd: parsed.totalCostUsd, usage: parsed.usage, filesIncluded, filesSkipped, timedOut: false, }; } // Fall back to extracting JSON from the response text const extracted = extractJson(parsed.response); if (!extracted) { return { response: parsed.response, valid: false, errors: "Could not extract JSON from response", model, sessionId: parsed.sessionId, totalCostUsd: parsed.totalCostUsd, usage: parsed.usage, filesIncluded, filesSkipped, timedOut: false, }; } return { response: extracted.raw, valid: true, model, sessionId: parsed.sessionId, totalCostUsd: parsed.totalCostUsd, usage: parsed.usage, filesIncluded, filesSkipped, timedOut: false, }; } - src/index.ts:182-230 (handler)Inline handler registered on the MCP server for the 'structured' tool. Calls executeStructured, handles session persistence, builds metadata, and formats errors/success responses.
async (input) => { const start = Date.now(); try { const result = await executeStructured(input); const sessionId = result.sessionId ?? input.sessionId; if (sessionId) { persist(sessionStore, sessionId, result); } const meta = buildMeta({ durationMs: Date.now() - start, model: result.model, sessionId: result.sessionId, totalCostUsd: result.totalCostUsd, usage: result.usage, timedOut: result.timedOut, }); if (!result.valid) { return { content: [{ type: "text" as const, text: `Error: ${result.errors ?? "Invalid response"}` }], isError: true, _meta: meta, }; } const content: Array<{ type: "text"; text: string }> = [ { type: "text", text: result.response }, ]; const textMeta: string[] = []; if (result.filesIncluded.length > 0) textMeta.push(`Files: ${result.filesIncluded.join(", ")}`); if (result.timedOut) textMeta.push("(timed out)"); if (textMeta.length > 0) { content.push({ type: "text", text: textMeta.join("\n") }); } return { content, _meta: meta }; } catch (e) { console.error("[structured]", e); return { content: [{ type: "text" as const, text: `Error: ${getErrorMessage(e)}` }], isError: true, _meta: buildMeta({ durationMs: Date.now() - start }), }; } }, ); - src/tools/structured.ts:10-33 (schema)StructuredInput interface (prompt, schema, files, model, sessionId, etc.) and StructuredResult interface (response, valid, errors, model, sessionId, usage, etc.) defining the tool's input/output types.
export interface StructuredInput { prompt: string; schema: string; files?: string[]; model?: string; sessionId?: string; noSessionPersistence?: boolean; workingDirectory?: string; timeout?: number; maxBudgetUsd?: number; } export interface StructuredResult { response: string; valid: boolean; errors?: string; model?: string; sessionId?: string; totalCostUsd?: number; usage?: ClaudeUsage; filesIncluded: string[]; filesSkipped: string[]; timedOut: boolean; } - src/index.ts:145-180 (schema)Registration of the 'structured' tool on the MCP server including title, description, and Zod-based inputSchema with fields: prompt, schema, files, model, sessionId, noSessionPersistence, workingDirectory, timeout, maxBudgetUsd.
server.registerTool( "structured", { title: "Structured Output", description: structuredDescription, inputSchema: { prompt: z.string().describe("What to generate or extract"), schema: z.string().describe("JSON Schema as a JSON string"), files: z .array(z.string()) .optional() .describe("Text file paths to include as context"), model: z.string().optional().describe("Model alias or full Claude model name"), sessionId: z .string() .optional() .describe("Claude session ID to resume with --resume"), 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)"), maxBudgetUsd: z .number() .positive() .optional() .describe("Maximum cost budget in USD for this call (passed to --max-budget-usd)"), }, annotations: structuredAnnotations, - src/index.ts:143-230 (registration)Registration block: calls server.registerTool('structured', ...) with schema, annotations, and the handler callback.
// --- structured tool --- server.registerTool( "structured", { title: "Structured Output", description: structuredDescription, inputSchema: { prompt: z.string().describe("What to generate or extract"), schema: z.string().describe("JSON Schema as a JSON string"), files: z .array(z.string()) .optional() .describe("Text file paths to include as context"), model: z.string().optional().describe("Model alias or full Claude model name"), sessionId: z .string() .optional() .describe("Claude session ID to resume with --resume"), 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)"), maxBudgetUsd: z .number() .positive() .optional() .describe("Maximum cost budget in USD for this call (passed to --max-budget-usd)"), }, annotations: structuredAnnotations, }, async (input) => { const start = Date.now(); try { const result = await executeStructured(input); const sessionId = result.sessionId ?? input.sessionId; if (sessionId) { persist(sessionStore, sessionId, result); } const meta = buildMeta({ durationMs: Date.now() - start, model: result.model, sessionId: result.sessionId, totalCostUsd: result.totalCostUsd, usage: result.usage, timedOut: result.timedOut, }); if (!result.valid) { return { content: [{ type: "text" as const, text: `Error: ${result.errors ?? "Invalid response"}` }], isError: true, _meta: meta, }; } const content: Array<{ type: "text"; text: string }> = [ { type: "text", text: result.response }, ]; const textMeta: string[] = []; if (result.filesIncluded.length > 0) textMeta.push(`Files: ${result.filesIncluded.join(", ")}`); if (result.timedOut) textMeta.push("(timed out)"); if (textMeta.length > 0) { content.push({ type: "text", text: textMeta.join("\n") }); } return { content, _meta: meta }; } catch (e) { console.error("[structured]", e); return { content: [{ type: "text" as const, text: `Error: ${getErrorMessage(e)}` }], isError: true, _meta: buildMeta({ durationMs: Date.now() - start }), }; } }, );