UNPKG

aiwg

Version:

Cognitive architecture for AI-augmented software development with structured memory, ensemble validation, and closed-loop correction. FAIR-aligned artifacts, 84% cost reduction via human-in-the-loop, standards adopted by 100+ organizations.

307 lines (280 loc) 8.71 kB
/** * MCPsmith Server Generator * * Generates complete MCP servers from analyzer results. * * @architecture @.aiwg/architecture/mcpsmith-architecture.md - Section 5.5 * @implements @.aiwg/architecture/decisions/ADR-014-mcpsmith-mcp-server-generator.md */ import fs from 'fs/promises'; import path from 'path'; import { fileURLToPath } from 'url'; import crypto from 'crypto'; import type { GeneratorOptions, GeneratedServer, MCPServerConfig, MCPServerManifest, MCPToolDefinition } from './types.js'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); /** * Generate MCP server from analyzer result */ export async function generateServer(options: GeneratorOptions): Promise<GeneratedServer> { const { serverId, serverName, description, analyzerResult, config, outputDir } = options; // Create server directory const serverPath = path.join(outputDir, serverId); await fs.mkdir(serverPath, { recursive: true }); await fs.mkdir(path.join(serverPath, 'tools'), { recursive: true }); // Generate config const serverConfig = createConfig(serverId, serverName, description, analyzerResult, config); const configPath = path.join(serverPath, 'config.json'); await fs.writeFile(configPath, JSON.stringify(serverConfig, null, 2), 'utf-8'); // Generate tool definition files const toolPaths: string[] = []; for (const tool of analyzerResult.tools) { const toolPath = path.join(serverPath, 'tools', `${tool.name}.json`); await fs.writeFile(toolPath, JSON.stringify(tool, null, 2), 'utf-8'); toolPaths.push(toolPath); } // Generate server.mjs from template const serverCode = await generateServerCode(serverId, serverName, analyzerResult); const serverCodePath = path.join(serverPath, 'server.mjs'); await fs.writeFile(serverCodePath, serverCode, 'utf-8'); await fs.chmod(serverCodePath, 0o755); // Make executable // Generate manifest const manifest = createManifest(serverId, serverName, analyzerResult, serverConfig); const manifestPath = path.join(serverPath, 'manifest.json'); await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2), 'utf-8'); // Create .mcpsmith marker file await fs.writeFile( path.join(serverPath, '.mcpsmith'), JSON.stringify({ generated_at: new Date().toISOString(), mcpsmith_version: '1.0.0' }, null, 2), 'utf-8' ); return { id: serverId, path: serverPath, files: { server: serverCodePath, manifest: manifestPath, config: configPath, tools: toolPaths } }; } /** * Create server configuration */ function createConfig( serverId: string, serverName: string, description: string, analyzerResult: any, userConfig: Partial<MCPServerConfig> ): MCPServerConfig { const sourceCommand = analyzerResult.metadata.sourceName; return { $schema: 'https://aiwg.io/schemas/mcpsmith-server-config.json', server: { name: serverName, version: userConfig.server?.version || '1.0.0', description }, transport: userConfig.transport || { type: 'stdio', options: {} }, source: { type: analyzerResult.metadata.sourceType, command: sourceCommand, workingDirectory: userConfig.source?.workingDirectory || process.cwd(), environment: userConfig.source?.environment || {}, timeout: userConfig.source?.timeout || 30000 }, tools: { prefix: userConfig.tools?.prefix || `${serverId}-`, allowlist: userConfig.tools?.allowlist || analyzerResult.tools.map((t: MCPToolDefinition) => t.name), denylist: userConfig.tools?.denylist || [], dangerousCommands: { require_confirmation: userConfig.tools?.dangerousCommands?.require_confirmation || analyzerResult.tools.filter((t: MCPToolDefinition) => t.metadata.requiresConfirmation).map((t: MCPToolDefinition) => t.name), blocked: userConfig.tools?.dangerousCommands?.blocked || [] } }, resources: { enabled: userConfig.resources?.enabled ?? false, patterns: userConfig.resources?.patterns || [] }, prompts: { enabled: userConfig.prompts?.enabled ?? false }, security: { sandboxed: userConfig.security?.sandboxed ?? true, maxExecutionTime: userConfig.security?.maxExecutionTime || 60000, allowedPaths: userConfig.security?.allowedPaths || [process.cwd()], blockedCommands: userConfig.security?.blockedCommands || [], requireConfirmation: userConfig.security?.requireConfirmation || analyzerResult.tools.filter((t: MCPToolDefinition) => t.metadata.requiresConfirmation).map((t: MCPToolDefinition) => t.name) }, logging: { enabled: userConfig.logging?.enabled ?? true, level: userConfig.logging?.level || 'info', maxFiles: userConfig.logging?.maxFiles || 10, maxSize: userConfig.logging?.maxSize || '10mb' } }; } /** * Create server manifest */ function createManifest( serverId: string, serverName: string, analyzerResult: any, config: MCPServerConfig ): MCPServerManifest { const tools = analyzerResult.tools.map((t: MCPToolDefinition) => ({ name: t.name, description: t.description, inputSchema: t.inputSchema, outputSchema: t.outputSchema, dangerous: t.metadata.dangerous, requiresConfirmation: t.metadata.requiresConfirmation })); const files = [ 'server.mjs', 'config.json', 'manifest.json', '.mcpsmith', ...analyzerResult.tools.map((t: MCPToolDefinition) => `tools/${t.name}.json`) ]; const checksum = crypto.createHash('sha256') .update(JSON.stringify({ serverId, config, tools })) .digest('hex'); return { $schema: 'https://aiwg.io/schemas/mcpsmith-manifest.json', id: serverId, name: serverName, version: config.server.version, mcpVersion: '2025-11-25', generated: { at: new Date().toISOString(), by: 'mcpsmith/1.0.0', from: { type: analyzerResult.metadata.sourceType, source: analyzerResult.metadata.sourceName, version: analyzerResult.metadata.sourceVersion } }, capabilities: { tools: true, resources: config.resources.enabled, prompts: config.prompts.enabled, sampling: false, logging: config.logging.enabled }, tools, resources: [], prompts: [], dependencies: { runtime: ['node >= 18.0.0'], external: [ `${analyzerResult.metadata.sourceName}${analyzerResult.metadata.sourceVersion ? ' >= ' + analyzerResult.metadata.sourceVersion : ''}` ] }, files, checksum }; } /** * Generate server code from template */ async function generateServerCode( serverId: string, serverName: string, analyzerResult: any ): Promise<string> { // Load template const templatePath = path.join( path.dirname(__dirname), '..', '..', '.aiwg', 'smiths', 'mcpsmith', 'templates', 'server-template.mjs' ); let template = await fs.readFile(templatePath, 'utf-8'); // Replace template variables const now = new Date().toISOString(); const replacements: Record<string, string> = { '{{SERVER_ID}}': serverId, '{{SERVER_NAME}}': serverName, '{{GENERATED_AT}}': now, '{{SOURCE_TYPE}}': analyzerResult.metadata.sourceType, '{{SOURCE_COMMAND}}': analyzerResult.metadata.sourceName, '{{MCPSMITH_VERSION}}': '1.0.0' }; for (const [key, value] of Object.entries(replacements)) { template = template.replace(new RegExp(key, 'g'), value); } return template; } /** * Update registry with new server */ export async function updateRegistry( registryPath: string, serverId: string, serverInfo: any ): Promise<void> { let registry; try { const content = await fs.readFile(registryPath, 'utf-8'); registry = JSON.parse(content); } catch { // Create new registry registry = { $schema: 'https://aiwg.io/schemas/mcpsmith-registry.json', version: '1.0', servers: [], integration: { aiwgMcp: { enabled: true, autoRegister: true, resourcePrefix: 'mcpsmith://' }, platforms: { claude: false, factory: false, codex: false, cursor: false } }, defaults: { transport: 'stdio', sandbox: true, timeout: 30000 } }; } // Remove existing entry if present registry.servers = registry.servers.filter((s: any) => s.id !== serverId); // Add new entry registry.servers.push(serverInfo); // Write back await fs.writeFile(registryPath, JSON.stringify(registry, null, 2), 'utf-8'); }