scaffold_consumer
Generate type-safe client code from API schemas with full type inference for TypeScript functions, React hooks, or Zustand actions.
Instructions
Generate type-safe client code from API schemas. Creates TypeScript functions, React hooks, or Zustand actions with full type inference.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| producerDir | Yes | Path to API source directory | |
| toolName | Yes | Name of the endpoint/tool to scaffold | |
| target | No | Output target format | |
| includeErrorHandling | No | Include try/catch error handling | |
| includeTypes | No | Include TypeScript type definitions |
Implementation Reference
- src/index.ts:193-207 (registration)Tool registration for 'scaffold_consumer' as an MCP tool with name, description, and input schema definition.
{ name: 'scaffold_consumer', description: 'Generate type-safe client code from API schemas. Creates TypeScript functions, React hooks, or Zustand actions with full type inference.', inputSchema: { type: 'object', properties: { producerDir: { type: 'string', description: 'Path to API source directory' }, toolName: { type: 'string', description: 'Name of the endpoint/tool to scaffold' }, target: { type: 'string', enum: ['typescript', 'javascript', 'react-hook', 'zustand-action'], description: 'Output target format' }, includeErrorHandling: { type: 'boolean', description: 'Include try/catch error handling' }, includeTypes: { type: 'boolean', description: 'Include TypeScript type definitions' }, }, required: ['producerDir', 'toolName'], }, }, - src/index.ts:73-79 (schema)Zod schema (ScaffoldConsumerInput) defining the input parameters for scaffold_consumer: producerDir, toolName, target, includeErrorHandling, includeTypes.
const ScaffoldConsumerInput = z.object({ producerDir: z.string().describe('Path to MCP server source directory'), toolName: z.string().describe('Name of the tool to scaffold consumer for'), target: z.enum(['typescript', 'javascript', 'react-hook', 'zustand-action']).optional().describe('Output target format (default: typescript)'), includeErrorHandling: z.boolean().optional().describe('Include try/catch error handling (default: true)'), includeTypes: z.boolean().optional().describe('Include TypeScript type definitions (default: true)'), }); - src/index.ts:405-442 (handler)Handler logic: extracts producer schemas, finds the requested tool, calls scaffoldConsumerFromProducer, and returns generated code.
case 'scaffold_consumer': { const input = ScaffoldConsumerInput.parse(args); log(`Scaffolding consumer for tool: ${input.toolName}`); // Extract producer schemas to find the requested tool const producers = await extractProducerSchemas({ rootDir: input.producerDir }); const producer = producers.find(p => p.toolName === input.toolName); if (!producer) { throw new Error(`Tool "${input.toolName}" not found in ${input.producerDir}`); } const result = scaffoldConsumerFromProducer(producer, { target: input.target || 'typescript', includeErrorHandling: input.includeErrorHandling ?? true, includeTypes: input.includeTypes ?? true, includeJSDoc: true, }); log(`Generated ${input.target || 'typescript'} consumer code`); return { content: [ { type: 'text', text: JSON.stringify({ success: true, toolName: input.toolName, target: input.target || 'typescript', suggestedFilename: result.suggestedFilename, code: result.code, types: result.types, example: result.example, }, null, 2), }, ], }; } - src/tools/scaffold.ts:35-79 (handler)Core implementation of scaffoldConsumerFromProducer: generates consumer code (TypeScript, JavaScript, React hook, or Zustand action) from a producer schema.
export function scaffoldConsumerFromProducer( producer: ProducerSchema, options: ScaffoldOptions = { target: 'typescript' } ): ScaffoldResult { const { target, includeErrorHandling = true, includeTypes = true, functionPrefix = '', includeJSDoc = true } = options; const toolName = producer.toolName; const functionName = `${functionPrefix}${toCamelCase(toolName)}`; const inputProps = producer.inputSchema.properties || {}; const requiredArgs = producer.inputSchema.required || []; // Generate TypeScript interface for args const argsInterface = generateArgsInterface(toolName, inputProps, requiredArgs); // Generate the function based on target let code: string; let types: string | undefined; let example: string; switch (target) { case 'react-hook': ({ code, types, example } = generateReactHook(toolName, functionName, inputProps, requiredArgs, { includeErrorHandling, includeTypes, includeJSDoc, producer })); break; case 'zustand-action': ({ code, types, example } = generateZustandAction(toolName, functionName, inputProps, requiredArgs, { includeErrorHandling, includeTypes, includeJSDoc, producer })); break; case 'javascript': const jsResult = generateJavaScript(toolName, functionName, inputProps, requiredArgs, { includeErrorHandling, includeJSDoc, producer }); code = jsResult.code; example = jsResult.example; types = undefined; break; case 'typescript': default: ({ code, types, example } = generateTypeScript(toolName, functionName, inputProps, requiredArgs, { includeErrorHandling, includeTypes, includeJSDoc, producer })); break; } return { code, suggestedFilename: `use-${toKebabCase(toolName)}.${target === 'javascript' ? 'js' : 'ts'}`, types, example, }; } - src/tools/scaffold.ts:132-467 (helper)Helper generator functions: generateTypeScript, generateJavaScript, generateReactHook, generateZustandAction — all called by scaffoldConsumerFromProducer to produce target-specific code.
// ============================================================================ // Generator Functions // ============================================================================ function generateTypeScript( toolName: string, functionName: string, inputProps: Record<string, JSONSchema>, requiredArgs: string[], options: { includeErrorHandling: boolean; includeTypes: boolean; includeJSDoc: boolean; producer: ProducerSchema } ): { code: string; types?: string; example: string } { const { includeErrorHandling, includeTypes, includeJSDoc, producer } = options; const argsType = `${toPascalCase(toolName)}Args`; const resultType = `${toPascalCase(toolName)}Result`; const types = includeTypes ? ` export interface ${argsType} { ${Object.entries(inputProps) .map(([key, schema]) => ` ${key}${requiredArgs.includes(key) ? '' : '?'}: ${jsonSchemaToTsType(schema)};`) .join('\n')} } export interface ${resultType} { // TODO: Define based on actual response [key: string]: unknown; } `.trim() : undefined; const jsdoc = includeJSDoc ? ` /** * ${producer.description || `Call the ${toolName} tool`} * @trace-contract CONSUMER * Producer: ${producer.location.file}:${producer.location.line} */` : ''; const code = ` ${jsdoc} export async function ${functionName}( client: McpClient, args: ${includeTypes ? argsType : `{ ${Object.keys(inputProps).join(', ')} }`} ): Promise<${includeTypes ? resultType : 'unknown'}> { ${includeErrorHandling ? ` try { const result = await client.callTool('${toolName}', args); return JSON.parse(result.content[0].text); } catch (error) { console.error('Error calling ${toolName}:', error); throw error; }` : ` const result = await client.callTool('${toolName}', args); return JSON.parse(result.content[0].text);`} } `.trim(); const example = ` // Usage: const result = await ${functionName}(client, { ${requiredArgs.map(arg => ` ${arg}: /* ${jsonSchemaToTsType(inputProps[arg])} */,`).join('\n')} }); `.trim(); return { code, types, example }; } function generateJavaScript( toolName: string, functionName: string, inputProps: Record<string, JSONSchema>, requiredArgs: string[], options: { includeErrorHandling: boolean; includeJSDoc: boolean; producer: ProducerSchema } ): { code: string; example: string } { const { includeErrorHandling, includeJSDoc, producer } = options; const jsdoc = includeJSDoc ? ` /** * ${producer.description || `Call the ${toolName} tool`} * @param {Object} client - MCP client * @param {Object} args - Tool arguments ${Object.entries(inputProps) .map(([key, schema]) => ` * @param {${jsonSchemaToJsType(schema)}} args.${key}`) .join('\n')} * @returns {Promise<Object>} */` : ''; const code = ` ${jsdoc} export async function ${functionName}(client, args) { ${includeErrorHandling ? ` try { const result = await client.callTool('${toolName}', args); return JSON.parse(result.content[0].text); } catch (error) { console.error('Error calling ${toolName}:', error); throw error; }` : ` const result = await client.callTool('${toolName}', args); return JSON.parse(result.content[0].text);`} } `.trim(); const example = ` // Usage: const result = await ${functionName}(client, { ${requiredArgs.map(arg => ` ${arg}: /* value */,`).join('\n')} }); `.trim(); return { code, example }; } function generateReactHook( toolName: string, _functionName: string, inputProps: Record<string, JSONSchema>, requiredArgs: string[], options: { includeErrorHandling: boolean; includeTypes: boolean; includeJSDoc: boolean; producer: ProducerSchema } ): { code: string; types?: string; example: string } { const { includeTypes, producer } = options; const hookName = `use${toPascalCase(toolName)}`; const argsType = `${toPascalCase(toolName)}Args`; const types = includeTypes ? ` export interface ${argsType} { ${Object.entries(inputProps) .map(([key, schema]) => ` ${key}${requiredArgs.includes(key) ? '' : '?'}: ${jsonSchemaToTsType(schema)};`) .join('\n')} } `.trim() : undefined; const code = ` import { useState, useCallback } from 'react'; import { useMcpClient } from './mcp-context'; // Adjust import /** * ${producer.description || `Hook for ${toolName} tool`} * @trace-contract CONSUMER (React Hook) * Producer: ${producer.location.file}:${producer.location.line} */ export function ${hookName}() { const client = useMcpClient(); const [loading, setLoading] = useState(false); const [error, setError] = useState<Error | null>(null); const [data, setData] = useState<unknown>(null); const execute = useCallback(async (args: ${includeTypes ? argsType : 'Record<string, unknown>'}) => { setLoading(true); setError(null); try { const result = await client.callTool('${toolName}', args); const parsed = JSON.parse(result.content[0].text); setData(parsed); return parsed; } catch (err) { setError(err instanceof Error ? err : new Error(String(err))); throw err; } finally { setLoading(false); } }, [client]); return { execute, loading, error, data }; } `.trim(); const example = ` // Usage in React component: function MyComponent() { const { execute, loading, error, data } = ${hookName}(); const handleClick = async () => { await execute({ ${requiredArgs.map(arg => ` ${arg}: /* value */,`).join('\n')} }); }; if (loading) return <div>Loading...</div>; if (error) return <div>Error: {error.message}</div>; return <div>{JSON.stringify(data)}</div>; } `.trim(); return { code, types, example }; } function generateZustandAction( toolName: string, _functionName: string, inputProps: Record<string, JSONSchema>, requiredArgs: string[], options: { includeErrorHandling: boolean; includeTypes: boolean; includeJSDoc: boolean; producer: ProducerSchema } ): { code: string; types?: string; example: string } { const { includeTypes, producer } = options; const actionName = toCamelCase(toolName); const argsType = `${toPascalCase(toolName)}Args`; const types = includeTypes ? ` export interface ${argsType} { ${Object.entries(inputProps) .map(([key, schema]) => ` ${key}${requiredArgs.includes(key) ? '' : '?'}: ${jsonSchemaToTsType(schema)};`) .join('\n')} } `.trim() : undefined; const code = ` /** * Zustand action for ${toolName} * @trace-contract CONSUMER (Zustand) * Producer: ${producer.location.file}:${producer.location.line} */ export const create${toPascalCase(toolName)}Slice = (set: any, get: any) => ({ ${actionName}Loading: false, ${actionName}Error: null as Error | null, ${actionName}Data: null as unknown, ${actionName}: async (args: ${includeTypes ? argsType : 'Record<string, unknown>'}) => { const { mcpClient } = get(); set({ ${actionName}Loading: true, ${actionName}Error: null }); try { const result = await mcpClient.callTool('${toolName}', args); const data = JSON.parse(result.content[0].text); set({ ${actionName}Data: data, ${actionName}Loading: false }); return data; } catch (error) { set({ ${actionName}Error: error instanceof Error ? error : new Error(String(error)), ${actionName}Loading: false }); throw error; } }, }); `.trim(); const example = ` // Add to your Zustand store: import { create } from 'zustand'; const useStore = create((set, get) => ({ mcpClient: null, setMcpClient: (client) => set({ mcpClient: client }), ...create${toPascalCase(toolName)}Slice(set, get), })); // Usage: const { ${actionName}, ${actionName}Loading, ${actionName}Data } = useStore(); await ${actionName}({ ${requiredArgs.map(arg => ` ${arg}: /* value */,`).join('\n')} }); `.trim(); return { code, types, example }; } // ============================================================================ // Utilities // ============================================================================ function generateArgsInterface(toolName: string, props: Record<string, JSONSchema>, required: string[]): string { const lines = Object.entries(props).map(([key, schema]) => { const optional = required.includes(key) ? '' : '?'; const type = jsonSchemaToTsType(schema); return ` ${key}${optional}: ${type};`; }); return `interface ${toPascalCase(toolName)}Args {\n${lines.join('\n')}\n}`; } function jsonSchemaToTsType(schema: JSONSchema): string { if (!schema.type) return 'unknown'; switch (schema.type) { case 'string': if (schema.enum && Array.isArray(schema.enum)) { return (schema.enum as unknown[]).map((v: unknown) => `'${v}'`).join(' | '); } return 'string'; case 'number': case 'integer': return 'number'; case 'boolean': return 'boolean'; case 'array': return `${jsonSchemaToTsType(schema.items || {})}[]`; case 'object': return 'Record<string, unknown>'; default: return 'unknown'; } } function jsonSchemaToJsType(schema: JSONSchema): string { if (!schema.type) return '*'; switch (schema.type) { case 'string': return 'string'; case 'number': case 'integer': return 'number'; case 'boolean': return 'boolean'; case 'array': return 'Array'; case 'object': return 'Object'; default: return '*'; } } function inferSchemaFromArgs(args: Record<string, unknown>): Record<string, string> { const schema: Record<string, string> = {}; for (const [key, value] of Object.entries(args)) { if (typeof value === 'string' || value === '<value>') { schema[key] = 'z.string()'; } else if (typeof value === 'number') { schema[key] = 'z.number()'; } else if (typeof value === 'boolean') { schema[key] = 'z.boolean()'; } else if (Array.isArray(value)) { schema[key] = 'z.array(z.unknown())'; } else if (typeof value === 'object') { schema[key] = 'z.object({})'; } else { schema[key] = 'z.unknown()'; } } return schema; } function toCamelCase(str: string): string { return str.replace(/_([a-z])/g, (_, c) => c.toUpperCase()); } function toPascalCase(str: string): string { const camel = toCamelCase(str); return camel.charAt(0).toUpperCase() + camel.slice(1); } function toKebabCase(str: string): string { return str.replace(/_/g, '-').toLowerCase(); }