codemesh
Version:
Execute TypeScript code against multiple MCP servers, weaving them together into powerful workflows
505 lines (489 loc) • 22.3 kB
JavaScript
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;
`;
}
}