UNPKG

codemesh

Version:

Execute TypeScript code against multiple MCP servers, weaving them together into powerful workflows

505 lines (489 loc) 22.3 kB
import { compile } from 'json-schema-to-typescript'; import { toPascalCase, createServerObjectName, convertToolName, convertServerName, createSafeFunctionName, } from './utils.js'; import { logger } from './logger.js'; import { loadAugmentations, getAugmentation } from './augmentation.js'; export class TypeGeneratorService { static instance; constructor() { } static getInstance() { if (!TypeGeneratorService.instance) { TypeGeneratorService.instance = new TypeGeneratorService(); } return TypeGeneratorService.instance; } /** * Generate TypeScript types from discovered tools */ async generateTypes(discoveryResults, codemeshDir) { logger.log(`🔧 Generating TypeScript types for discovered tools...`); // Load augmentations if directory is provided const augmentations = codemeshDir ? loadAugmentations(codemeshDir) : new Map(); const generatedTools = []; const typeDefinitions = []; const functionSignatures = []; // Process each successful discovery result for (const result of discoveryResults) { if (!result.success) { logger.log(`⚠️ Skipping ${result.serverName} due to discovery failure`); continue; } logger.log(`📝 Processing ${result.tools.length} tools from ${result.serverName}...`); for (const tool of result.tools) { try { const generatedTool = await this.generateToolType(tool); generatedTools.push(generatedTool); typeDefinitions.push(generatedTool.inputTypeDefinition); if (generatedTool.outputTypeDefinition) { typeDefinitions.push(generatedTool.outputTypeDefinition); } functionSignatures.push(generatedTool.functionSignature); logger.log(`✅ Generated types for ${tool.name}`); } catch (error) { logger.error(`❌ Failed to generate types for ${tool.name}:`, error); } } } // Generate clean namespaced types and server interfaces const namespacedTypes = this.generateNamespacedTypes(generatedTools, augmentations); // Use only namespaced types const combinedTypes = namespacedTypes; // Generate tools namespace with only namespaced server objects const toolsNamespace = this.generateNamespacedToolsNamespace(generatedTools); logger.log(`🎯 Generated TypeScript types for ${generatedTools.length} tools`); return { tools: generatedTools, combinedTypes, toolsNamespace, }; } /** * Generate TypeScript types for a single tool */ async generateToolType(tool) { // Create a safe type name from tool name const inputTypeName = this.createSafeTypeName(tool.name, tool.serverId, 'Input'); const outputTypeName = this.createSafeTypeName(tool.name, tool.serverId, 'Output'); // Convert input JSON schema to TypeScript interface let inputTypeDefinition; try { if (tool.inputSchema && typeof tool.inputSchema === 'object') { inputTypeDefinition = await compile(tool.inputSchema, inputTypeName, { bannerComment: `// Input type for ${tool.name} tool from ${tool.serverName}`, style: { singleQuote: false, }, }); } else { // Fallback for tools without schemas inputTypeDefinition = `// Input type for ${tool.name} tool from ${tool.serverName}\nexport interface ${inputTypeName} {}\n`; } } catch (error) { logger.warn(`⚠️ Failed to generate input schema for ${tool.name}, using empty interface:`, error); inputTypeDefinition = `// Input type for ${tool.name} tool from ${tool.serverName}\nexport interface ${inputTypeName} {}\n`; } // Convert output JSON schema to TypeScript interface (if present) let outputTypeDefinition; if (tool.outputSchema && typeof tool.outputSchema === 'object') { try { outputTypeDefinition = await compile(tool.outputSchema, outputTypeName, { bannerComment: `// Output type for ${tool.name} tool from ${tool.serverName}`, style: { singleQuote: false, }, }); logger.log(`✨ Generated output type for ${tool.name}`); } catch (error) { logger.warn(`⚠️ Failed to generate output schema for ${tool.name}:`, error); outputTypeDefinition = undefined; } } // Generate function signature const functionSignature = this.generateFunctionSignature(tool, inputTypeName, outputTypeDefinition ? outputTypeName : undefined); // Generate namespaced properties const namespacedServerName = convertServerName(tool.serverId); const namespacedInputTypeName = this.createNamespacedTypeName(tool.name, 'Input'); const namespacedOutputTypeName = outputTypeDefinition ? this.createNamespacedTypeName(tool.name, 'Output') : undefined; const camelCaseMethodName = convertToolName(tool.name); const serverObjectName = createServerObjectName(tool.serverId); return { toolName: tool.name, serverId: tool.serverId, serverName: tool.serverName, inputTypeName, inputTypeDefinition, outputTypeName: outputTypeDefinition ? outputTypeName : undefined, outputTypeDefinition, functionSignature, // New namespaced properties namespacedServerName, namespacedInputTypeName, namespacedOutputTypeName, camelCaseMethodName, serverObjectName, // Store original schemas for JSDoc inputSchema: tool.inputSchema, outputSchema: tool.outputSchema, description: tool.description, }; } /** * Create a safe TypeScript type name from tool name and server ID */ createSafeTypeName(toolName, serverId, suffix) { // Convert to PascalCase and make it unique const safeName = toolName .replace(/[^a-zA-Z0-9]/g, '_') .split('_') .map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()) .join(''); const safeServerId = serverId .replace(/[^a-zA-Z0-9]/g, '_') .split('_') .map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()) .join(''); return `${safeName}${safeServerId}${suffix}`; } /** * Create a namespaced type name for use within server namespace */ createNamespacedTypeName(toolName, suffix) { // Simple PascalCase name for use within namespace const safeName = toPascalCase(toolName); return `${safeName}${suffix}`; } /** * Generate a TypeScript function signature for a tool */ generateFunctionSignature(tool, inputTypeName, outputTypeName) { const description = tool.description ? `\n /**\n * ${tool.description}\n * Server: ${tool.serverName}\n */` : ''; // Use structured output type if available, otherwise fall back to ToolResult const returnType = outputTypeName ? `Promise<ToolResultWithOutput<${outputTypeName}>>` : 'Promise<ToolResult>'; return `${description} ${createSafeFunctionName(tool.name, tool.serverId)}(input: ${inputTypeName}): ${returnType};`; } /** * Generate detailed JSDoc comment for a tool method */ generateDetailedJSDoc(tool, namespacedInputType, namespacedOutputType, augmentation) { const lines = [' /**']; // Add main description if (tool.description) { lines.push(` * ${tool.description}`); lines.push(' *'); } // Add augmentation documentation if available if (augmentation) { // Split markdown into lines and indent for JSDoc const augLines = augmentation.documentation.split('\n'); for (const line of augLines) { if (line.trim()) { lines.push(` * ${line}`); } else { lines.push(' *'); } } lines.push(' *'); } // Add input schema details if (tool.inputSchema && typeof tool.inputSchema === 'object') { const schema = tool.inputSchema; if (schema.properties) { lines.push(' * @param input - Tool input parameters:'); for (const [propName, propSchema] of Object.entries(schema.properties)) { const prop = propSchema; const required = schema.required?.includes(propName) ? '(required)' : '(optional)'; const typeInfo = prop.type ? `{${prop.type}}` : ''; const description = prop.description || ''; lines.push(` * - ${propName} ${typeInfo} ${required} ${description}`.trim()); } lines.push(' *'); } } // Add output schema details - reference the type name instead of enumerating properties if (namespacedOutputType) { lines.push(` * @returns Tool result with structured output: {${namespacedOutputType}}`); lines.push(' *'); } else if (tool.outputSchema && typeof tool.outputSchema === 'object') { const schema = tool.outputSchema; lines.push(' * @returns Tool result with structured output'); if (schema.description) { lines.push(` * ${schema.description}`); } lines.push(' *'); } // Add server info lines.push(` * @server ${tool.serverName} (${tool.serverId})`); lines.push(' */'); return lines.join('\n'); } /** * Combine all type definitions into a single TypeScript module */ generateCombinedTypes(typeDefinitions) { const header = `// Generated TypeScript types for MCP tools // This file is auto-generated by CodeMesh - do not edit manually `; const toolResultType = `// Import CallToolResult from MCP SDK import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; // Re-export for consistency export type ToolResult = CallToolResult; export interface ToolResultWithOutput<T> extends ToolResult { structuredContent?: T; } `; // Legacy flat types for backwards compatibility const legacyTypes = `// Legacy flat types for backwards compatibility ${typeDefinitions.join('\n')} `; return header + toolResultType + legacyTypes; } /** * Generate namespaced types and server interfaces */ generateNamespacedTypes(tools, augmentations) { // Group tools by server const serverGroups = new Map(); for (const tool of tools) { const serverName = tool.namespacedServerName; if (!serverGroups.has(serverName)) { serverGroups.set(serverName, []); } serverGroups.get(serverName).push(tool); } // Add preamble with type definitions let namespacedTypes = `// Import CallToolResult from MCP SDK import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; // Re-export for consistency export type ToolResult = CallToolResult; export interface ToolResultWithOutput<T> extends ToolResult { structuredContent?: T; } `; // Generate namespace and interface for each server for (const [, serverTools] of serverGroups) { const serverObjectName = serverTools[0].serverObjectName; // Use PascalCase server object name as the type (e.g., WeatherServer) const typeName = serverObjectName.charAt(0).toUpperCase() + serverObjectName.slice(1); // Generate namespace with types namespacedTypes += `// ${typeName} namespace with input/output types\n`; namespacedTypes += `export namespace ${typeName} {\n`; // Add input/output types to namespace for (const tool of serverTools) { // Generate simplified types for namespace (without banners) namespacedTypes += ` export interface ${tool.namespacedInputTypeName} {\n`; // Extract the interface content from the generated type definition const inputInterface = this.extractInterfaceContent(tool.inputTypeDefinition, tool.inputTypeName); namespacedTypes += inputInterface.split('\n').map(line => ` ${line}`).join('\n'); namespacedTypes += `\n }\n\n`; if (tool.outputTypeDefinition && tool.namespacedOutputTypeName) { namespacedTypes += ` export interface ${tool.namespacedOutputTypeName} {\n`; const outputInterface = this.extractInterfaceContent(tool.outputTypeDefinition, tool.outputTypeName); namespacedTypes += outputInterface.split('\n').map(line => ` ${line}`).join('\n'); namespacedTypes += `\n }\n\n`; } } namespacedTypes += `}\n\n`; // Generate server interface namespacedTypes += `// ${typeName} interface with methods\n`; namespacedTypes += `export interface ${typeName} {\n`; for (const tool of serverTools) { const inputType = `${typeName}.${tool.namespacedInputTypeName}`; const returnType = tool.namespacedOutputTypeName ? `Promise<ToolResultWithOutput<${typeName}.${tool.namespacedOutputTypeName}>>` : 'Promise<ToolResult>'; // Generate detailed JSDoc with full schema information const namespacedOutputType = tool.namespacedOutputTypeName ? `${typeName}.${tool.namespacedOutputTypeName}` : undefined; // Get augmentation for this tool if available const augmentation = getAugmentation(augmentations, tool.serverObjectName, tool.camelCaseMethodName); const detailedJSDoc = this.generateDetailedJSDoc({ name: tool.toolName, description: tool.description, inputSchema: tool.inputSchema, outputSchema: tool.outputSchema, serverId: tool.serverId, serverName: tool.serverName, }, inputType, namespacedOutputType, augmentation); namespacedTypes += detailedJSDoc + '\n'; namespacedTypes += ` ${tool.camelCaseMethodName}(input: ${inputType}): ${returnType};\n\n`; } namespacedTypes += `}\n\n`; } return namespacedTypes; } /** * Extract interface content from generated type definition */ extractInterfaceContent(typeDefinition, typeName) { // Remove banner comments and extract just the interface body const lines = typeDefinition.split('\n'); const interfaceStartIndex = lines.findIndex(line => line.includes(`export interface ${typeName}`)); if (interfaceStartIndex === -1) return ' // No properties'; const interfaceStart = lines[interfaceStartIndex]; const openBraceIndex = interfaceStart.indexOf('{'); let content = ''; let braceCount = 0; let started = false; for (let i = interfaceStartIndex; i < lines.length; i++) { const line = lines[i]; for (const char of line) { if (char === '{') { braceCount++; started = true; } else if (char === '}') { braceCount--; } } if (started && braceCount > 0) { // Extract content inside the interface const lineContent = i === interfaceStartIndex ? line.substring(openBraceIndex + 1).trim() : line.trim(); if (lineContent) { content += ` ${lineContent}\n`; } } if (started && braceCount === 0) { break; } } return content || ' // No properties'; } /** * Generate the tools namespace with all function signatures */ generateToolsNamespace(functionSignatures, tools) { const header = `// Generated tools namespace for CodeMesh execution // This file is auto-generated by CodeMesh - do not edit manually export interface CodeMeshTools {`; // Group tools by server for server object declarations const serverGroups = new Map(); for (const tool of tools) { const serverObjectName = tool.serverObjectName; if (!serverGroups.has(serverObjectName)) { serverGroups.set(serverObjectName, []); } serverGroups.get(serverObjectName).push(tool); } // Generate server object declarations let serverDeclarations = '\n // Namespaced server objects\n'; for (const [serverObjectName, serverTools] of serverGroups) { const serverTypeName = serverTools[0].namespacedServerName; serverDeclarations += ` ${serverObjectName}: ${serverTypeName};\n`; } const footer = `} // Tool metadata for runtime resolution export const TOOL_METADATA = { ${tools .map((tool) => ` "${createSafeFunctionName(tool.toolName, tool.serverId)}": { originalName: "${tool.toolName}", serverId: "${tool.serverId}", serverName: "${tool.serverName}", }`) .join(',\n')} } as const; // Server metadata for namespaced API export const SERVER_METADATA = { ${Array.from(serverGroups.entries()) .map(([serverObjectName, serverTools]) => ` "${serverObjectName}": { serverId: "${serverTools[0].serverId}", serverName: "${serverTools[0].serverName}", namespacedServerName: "${serverTools[0].namespacedServerName}", }`) .join(',\n')} } as const; // Export for runtime use export type ToolName = keyof CodeMeshTools; export type ServerObjectName = keyof typeof SERVER_METADATA; `; return header + functionSignatures.join('\n') + serverDeclarations + '\n' + footer; } /** * Save generated types to files */ async saveGeneratedTypes(generatedTypes, outputDir) { const fs = await import('node:fs/promises'); const path = await import('node:path'); // Ensure output directory exists await fs.mkdir(outputDir, { recursive: true }); // Save combined types const typesPath = path.join(outputDir, 'types.ts'); await fs.writeFile(typesPath, generatedTypes.combinedTypes, 'utf-8'); logger.log(`📁 Saved types to ${typesPath}`); // Save tools namespace const toolsPath = path.join(outputDir, 'tools.ts'); await fs.writeFile(toolsPath, generatedTypes.toolsNamespace, 'utf-8'); logger.log(`📁 Saved tools namespace to ${toolsPath}`); // Save metadata as JSON for runtime use const metadataPath = path.join(outputDir, 'metadata.json'); const metadata = { generatedAt: new Date().toISOString(), tools: generatedTypes.tools.map((tool) => ({ toolName: tool.toolName, serverId: tool.serverId, serverName: tool.serverName, inputTypeName: tool.inputTypeName, functionName: createSafeFunctionName(tool.toolName, tool.serverId), })), }; await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2), 'utf-8'); logger.log(`📁 Saved metadata to ${metadataPath}`); } /** * Generate clean tools namespace with only namespaced server objects */ generateNamespacedToolsNamespace(tools) { // Group tools by server const serverGroups = new Map(); for (const tool of tools) { if (!serverGroups.has(tool.serverObjectName)) { serverGroups.set(tool.serverObjectName, []); } serverGroups.get(tool.serverObjectName).push(tool); } const serverObjects = Array.from(serverGroups.keys()) .map((serverObjectName) => { // Get the PascalCase type name (e.g., weatherServer → WeatherServer) const typeName = serverObjectName.charAt(0).toUpperCase() + serverObjectName.slice(1); return ` ${serverObjectName}: ${typeName};`; }) .join('\n'); const serverMetadata = Array.from(serverGroups.entries()) .map(([serverObjectName, serverTools]) => { const firstTool = serverTools[0]; // Use PascalCase server object name as the type (e.g., weatherServer → WeatherServer) const typeName = serverObjectName.charAt(0).toUpperCase() + serverObjectName.slice(1); return ` "${serverObjectName}": { serverId: "${firstTool.serverId}", serverName: "${firstTool.serverName}", namespacedServerName: "${typeName}", }`; }) .join(',\n'); return `// Generated tools namespace for CodeMesh execution // This file is auto-generated by CodeMesh - do not edit manually export interface CodeMeshTools { ${serverObjects} } // Server metadata for namespaced API export const SERVER_METADATA = { ${serverMetadata} } as const; // Export for runtime use export type ServerObjectName = keyof typeof SERVER_METADATA; `; } }