Skip to main content
Glama
scaffold.ts14.5 kB
/** * Contract Scaffold Tool * Generate consumer code from producer schema (or vice versa) */ import type { ProducerSchema, ConsumerSchema, JSONSchema } from '../types.js'; export interface ScaffoldOptions { /** Output language/framework */ target: 'typescript' | 'javascript' | 'react-hook' | 'zustand-action'; /** Include error handling */ includeErrorHandling?: boolean; /** Include TypeScript types */ includeTypes?: boolean; /** Function name prefix */ functionPrefix?: string; /** Add JSDoc comments */ includeJSDoc?: boolean; } export interface ScaffoldResult { /** Generated code */ code: string; /** Suggested filename */ suggestedFilename: string; /** TypeScript type definitions (if applicable) */ types?: string; /** Usage example */ example: string; } /** * Generate consumer code 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, }; } /** * Generate producer schema stub from consumer usage */ export function scaffoldProducerFromConsumer( consumer: ConsumerSchema, options: { includeHandler?: boolean } = {} ): ScaffoldResult { const { includeHandler = true } = options; const toolName = consumer.toolName; const args = consumer.argumentsProvided; // Infer types from argument values (basic inference) const inferredSchema = inferSchemaFromArgs(args); const code = ` import { z } from 'zod'; // Tool: ${toolName} // Scaffolded from consumer at ${consumer.callSite.file}:${consumer.callSite.line} // @trace-contract PRODUCER (scaffolded) server.tool( '${toolName}', 'TODO: Add description', { ${Object.entries(inferredSchema) .map(([key, type]) => ` ${key}: ${type},`) .join('\n')} }, ${includeHandler ? ` async (args) => { // TODO: Implement handler // Consumer expects these properties: ${consumer.expectedProperties.join(', ')} return { content: [{ type: 'text', text: JSON.stringify({ ${consumer.expectedProperties.map(p => ` ${p}: null, // TODO`).join('\n')} }) }] }; }` : ' async (args) => { /* TODO */ }'} ); `.trim(); return { code, suggestedFilename: `${toKebabCase(toolName)}-tool.ts`, example: `// This tool is called by:\n// ${consumer.callSite.file}:${consumer.callSite.line}`, }; } // ============================================================================ // 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(); }

Implementation Reference

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Mnehmos/mnehmos.trace.mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server