Node Omnibus MCP Server

  • src
#!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ErrorCode, ListResourcesRequestSchema, ListToolsRequestSchema, McpError, ReadResourceRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, PromptMessage } from '@modelcontextprotocol/sdk/types.js'; import { exec } from 'child_process'; import { promisify } from 'util'; import axios from 'axios'; import * as fs from 'fs/promises'; import * as path from 'path'; const execAsync = promisify(exec); // Type definitions for various tool arguments interface CreateProjectArgs extends Record<string, unknown> { name: string; type: 'react' | 'node' | 'next' | 'express' | 'fastify'; path: string; typescript?: boolean; } interface InstallPackageArgs extends Record<string, unknown> { packages: string[]; path: string; dev?: boolean; } interface RunScriptArgs extends Record<string, unknown> { script: string; path: string; } interface GenerateComponentArgs extends Record<string, unknown> { name: string; path: string; type: 'functional' | 'class'; props?: Record<string, string>; } interface CreateTypeDefinitionArgs extends Record<string, unknown> { name: string; path: string; properties: Record<string, string>; } interface AddScriptArgs extends Record<string, unknown> { path: string; name: string; command: string; } interface UpdateTsConfigArgs extends Record<string, unknown> { path: string; options: Record<string, unknown>; } interface CreateDocumentationArgs extends Record<string, unknown> { path: string; type: 'readme' | 'api' | 'component'; name?: string; } /** * NodeOmnibusServer class that provides comprehensive tooling for Node.js development */ class NodeOmnibusServer { private server: Server; private projectDocs: Map<string, string>; private prompts: Record<string, { name: string; description: string; arguments?: { name: string; description: string; required?: boolean }[]; }> = {}; // Initialize the property constructor() { this.server = new Server( { name: 'node-omnibus-server', version: '1.0.0', }, { capabilities: { resources: {}, tools: {}, prompts: {}, // Add prompts capability }, } ); this.projectDocs = new Map(); this.initializePrompts(); this.setupToolHandlers(); this.setupResourceHandlers(); this.setupPromptHandlers(); this.server.onerror = (error: Error) => console.error('[MCP Error]', error); process.on('SIGINT', async () => { await this.server.close(); process.exit(0); }); } private initializePrompts() { this.prompts = { 'create-project': { name: 'create-project', description: 'Guide through creating a new Node.js project with best practices', arguments: [ { name: 'projectType', description: 'Type of project to create (react, node, next, express, fastify)', required: true }, { name: 'features', description: 'Comma-separated list of features (e.g., typescript,testing,docker)', required: false } ] }, 'analyze-code': { name: 'analyze-code', description: 'Analyze code for potential improvements and best practices', arguments: [ { name: 'code', description: 'Code to analyze', required: true }, { name: 'language', description: 'Programming language', required: true } ] }, 'generate-component': { name: 'generate-component', description: 'Generate a React component with TypeScript support', arguments: [ { name: 'name', description: 'Component name', required: true }, { name: 'type', description: 'Component type (functional/class)', required: true } ] }, 'git-commit': { name: 'git-commit', description: 'Generate a descriptive Git commit message', arguments: [ { name: 'changes', description: 'Git diff or description of changes', required: true } ] }, 'debug-error': { name: 'debug-error', description: 'Get suggestions for debugging a Node.js error', arguments: [ { name: 'error', description: 'Error message or stack trace', required: true } ] } }; } private setupResourceHandlers() { // Handler for listing documentation resources this.server.setRequestHandler(ListResourcesRequestSchema, async () => ({ resources: Array.from(this.projectDocs.entries()).map(([id, content]) => ({ uri: `docs://${id}`, mimeType: 'text/markdown', name: `Documentation for ${id}`, description: `Project documentation and notes for ${id}`, })), })); // Handler for reading documentation content this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => { const url = new URL(request.params.uri); const id = url.hostname; const content = this.projectDocs.get(id); if (!content) { throw new McpError(ErrorCode.MethodNotFound, `Documentation not found for ${id}`); } return { contents: [{ uri: request.params.uri, mimeType: 'text/markdown', text: content, }], }; }); } private setupToolHandlers() { this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: 'create_project', description: 'Create a new Node.js project with enhanced configuration', inputSchema: { type: 'object', properties: { name: { type: 'string', description: 'Project name', }, type: { type: 'string', enum: ['react', 'node', 'next', 'express', 'fastify'], description: 'Project type', }, path: { type: 'string', description: 'Project directory path', }, typescript: { type: 'boolean', description: 'Enable TypeScript support', default: true, }, }, required: ['name', 'type', 'path'], }, }, { name: 'install_packages', description: 'Install npm packages with version management', inputSchema: { type: 'object', properties: { packages: { type: 'array', items: { type: 'string' }, description: 'Package names to install', }, path: { type: 'string', description: 'Project directory path', }, dev: { type: 'boolean', description: 'Install as dev dependency', default: false, }, }, required: ['packages', 'path'], }, }, { name: 'generate_component', description: 'Generate a new React component with TypeScript support', inputSchema: { type: 'object', properties: { name: { type: 'string', description: 'Component name', }, path: { type: 'string', description: 'Component directory path', }, type: { type: 'string', enum: ['functional', 'class'], description: 'Component type', }, props: { type: 'object', description: 'Component props with types', additionalProperties: { type: 'string' }, }, }, required: ['name', 'path', 'type'], }, }, { name: 'create_type_definition', description: 'Create TypeScript type definitions or interfaces', inputSchema: { type: 'object', properties: { name: { type: 'string', description: 'Type name', }, path: { type: 'string', description: 'File path', }, properties: { type: 'object', description: 'Type properties and their types', additionalProperties: { type: 'string' }, }, }, required: ['name', 'path', 'properties'], }, }, { name: 'add_script', description: 'Add a new npm script to package.json', inputSchema: { type: 'object', properties: { path: { type: 'string', description: 'Project directory path', }, name: { type: 'string', description: 'Script name', }, command: { type: 'string', description: 'Script command', }, }, required: ['path', 'name', 'command'], }, }, { name: 'update_tsconfig', description: 'Update TypeScript configuration', inputSchema: { type: 'object', properties: { path: { type: 'string', description: 'Project directory path', }, options: { type: 'object', description: 'TypeScript compiler options', additionalProperties: true, }, }, required: ['path', 'options'], }, }, { name: 'create_documentation', description: 'Generate project documentation', inputSchema: { type: 'object', properties: { path: { type: 'string', description: 'Project directory path', }, type: { type: 'string', enum: ['readme', 'api', 'component'], description: 'Documentation type', }, name: { type: 'string', description: 'Component or API name for specific documentation', }, }, required: ['path', 'type'], }, }, ], })); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { try { const args = request.params.arguments as Record<string, unknown>; switch (request.params.name) { case 'create_project': return await this.handleCreateProject(args as CreateProjectArgs); case 'install_packages': return await this.handleInstallPackages(args as InstallPackageArgs); case 'generate_component': return await this.handleGenerateComponent(args as GenerateComponentArgs); case 'create_type_definition': return await this.handleCreateTypeDefinition(args as CreateTypeDefinitionArgs); case 'add_script': return await this.handleAddScript(args as AddScriptArgs); case 'update_tsconfig': return await this.handleUpdateTsConfig(args as UpdateTsConfigArgs); case 'create_documentation': return await this.handleCreateDocumentation(args as CreateDocumentationArgs); default: throw new McpError( ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}` ); } } catch (error: unknown) { if (error instanceof McpError) throw error; throw new McpError( ErrorCode.InternalError, `Error executing ${request.params.name}: ${error instanceof Error ? error.message : String(error)}` ); } }); } private setupPromptHandlers() { // List prompts handler with proper schema this.server.setRequestHandler(ListPromptsRequestSchema, async () => ({ prompts: Object.values(this.prompts) })); // Get prompt handler with proper schema this.server.setRequestHandler(GetPromptRequestSchema, async (request) => { const { name, arguments: args } = request.params; const prompt = this.prompts[name]; if (!prompt) { throw new McpError(ErrorCode.InvalidParams, `Prompt not found: ${name}`); } // Validate required arguments prompt.arguments?.forEach(arg => { if (arg.required && !args?.[arg.name]) { throw new McpError(ErrorCode.InvalidParams, `Missing required argument: ${arg.name}`); } }); const messages = await this.generatePromptMessages(name, args || {}); return { messages }; }); } private async generatePromptMessages(name: string, args: Record<string, string>): Promise<PromptMessage[]> { switch (name) { case 'create-project': const features = args.features?.split(',').map(f => f.trim()) || []; return [ { role: 'user', content: { type: 'text', text: `Help me create a new ${args.projectType} project with these requirements: 1. Project Type: ${args.projectType} 2. Features: ${features.length > 0 ? features.join(', ') : 'basic setup'} Please provide: 1. Recommended project structure 2. Essential dependencies to include 3. Important configuration files 4. Best practices for this type of project 5. Common pitfalls to avoid ${features.includes('typescript') ? '6. TypeScript configuration recommendations' : ''} ${features.includes('testing') ? '7. Testing setup and frameworks' : ''} ${features.includes('docker') ? '8. Docker configuration guidance' : ''}` } } ]; case 'analyze-code': return [ { role: 'user', content: { type: 'text', text: `Please analyze this ${args.language} code for potential improvements and best practices:\n\n${args.code}` } } ]; case 'generate-component': return [ { role: 'user', content: { type: 'text', text: `Generate a ${args.type} React component named ${args.name} with TypeScript support. Include proper typing, error handling, and common best practices.` } } ]; case 'git-commit': return [ { role: 'user', content: { type: 'text', text: `Generate a concise but descriptive commit message following conventional commits format for these changes:\n\n${args.changes}` } } ]; case 'debug-error': return [ { role: 'user', content: { type: 'text', text: `Help me debug this Node.js error. Suggest potential causes and solutions:\n\n${args.error}` } } ]; default: throw new McpError(ErrorCode.InvalidParams, `Invalid prompt name: ${name}`); } } /** * Validates a path and creates it if it doesn't exist * @param path Directory path to validate/create * @throws McpError if path is invalid or cannot be created */ // Enhanced error handling for path validation private async validatePath(path: string): Promise<void> { try { // Check if path exists try { await fs.access(path); } catch { // If path doesn't exist, create it await fs.mkdir(path, { recursive: true }); return; } // If path exists, verify it's a directory const stats = await fs.stat(path); if (!stats.isDirectory()) { throw new McpError( ErrorCode.InvalidParams, `Path ${path} exists but is not a directory` ); } } catch (error) { if (error instanceof McpError) throw error; throw new McpError( ErrorCode.InvalidParams, `Failed to validate/create path ${path}: ${error instanceof Error ? error.message : String(error)}` ); } } private getProjectTemplate(type: string, name: string, typescript: boolean): { command: string; dependencies: string[]; devDependencies: string[]; } { const templates: Record<string, { command: string; dependencies: string[]; devDependencies: string[]; }> = { react: { command: typescript ? `npx create-react-app ./ --template typescript` : `npx create-react-app ./`, dependencies: ['react', 'react-dom'], devDependencies: typescript ? ['@types/react', '@types/react-dom', '@types/node'] : [], }, next: { command: `npx create-next-app@latest ./ ${typescript ? '--typescript' : ''} --tailwind --eslint`, dependencies: ['next', 'react', 'react-dom'], devDependencies: typescript ? ['@types/node', '@types/react', '@types/react-dom'] : [], }, express: { command: `npm init -y`, dependencies: ['express', 'cors', 'dotenv'], devDependencies: typescript ? ['typescript', '@types/node', '@types/express', '@types/cors', 'ts-node', 'nodemon'] : ['nodemon'], }, fastify: { command: `npm init -y`, dependencies: ['fastify', '@fastify/cors', '@fastify/env'], devDependencies: typescript ? ['typescript', '@types/node', 'ts-node', 'nodemon'] : ['nodemon'], }, node: { command: `npm init -y`, dependencies: [], devDependencies: typescript ? ['typescript', '@types/node', 'ts-node', 'nodemon'] : ['nodemon'], }, }; const template = templates[type]; if (!template) { throw new McpError(ErrorCode.InvalidParams, `Unsupported project type: ${type}`); } return template; } private async handleCreateProject(args: CreateProjectArgs) { try { // Create the project directory first const projectPath = path.join(args.path, args.name); await fs.mkdir(projectPath, { recursive: true }); const typescript = args.typescript !== false; const template = this.getProjectTemplate(args.type, args.name, typescript); // Execute project creation command in the project directory const { stdout: createOutput } = await execAsync(template.command, { cwd: projectPath }); // Install dependencies directly in the project directory if (template.dependencies.length > 0) { const installCmd = `npm install ${template.dependencies.join(' ')}`; await execAsync(installCmd, { cwd: projectPath }); } // Install dev dependencies if (template.devDependencies.length > 0) { const installDevCmd = `npm install --save-dev ${template.devDependencies.join(' ')}`; await execAsync(installDevCmd, { cwd: projectPath }); } // Setup TypeScript configuration if needed if (typescript) { const tsConfig = { compilerOptions: { target: "es2020", module: "commonjs", outDir: "./dist", rootDir: "./src", strict: true, esModuleInterop: true, skipLibCheck: true, forceConsistentCasingInFileNames: true, jsx: args.type === 'react' || args.type === 'next' ? "react-jsx" : undefined, }, include: ["src/**/*"], exclude: ["node_modules", "dist"] }; await fs.mkdir(path.join(projectPath, 'src'), { recursive: true }); await fs.writeFile( path.join(projectPath, 'tsconfig.json'), JSON.stringify(tsConfig, null, 2) ); } // Create initial documentation const readmeContent = this.generateReadme(args.name, args.type, typescript); await fs.writeFile( path.join(projectPath, 'README.md'), readmeContent ); // Store documentation in memory this.projectDocs.set(args.name, readmeContent); return { content: [ { type: 'text', text: `Project ${args.name} created successfully with ${typescript ? 'TypeScript' : 'JavaScript'} configuration`, }, ], }; } catch (error: unknown) { throw new McpError( ErrorCode.InternalError, `Failed to create project: ${error instanceof Error ? error.message : String(error)}` ); } } private generateReadme(name: string, type: string, typescript: boolean): string { return `# ${name} ## Description A ${type} project using ${typescript ? 'TypeScript' : 'JavaScript'}. ## Setup \`\`\`bash npm install \`\`\` ## Development \`\`\`bash npm run dev \`\`\` ## Build \`\`\`bash npm run build \`\`\` ## Project Structure - \`src/\` - Source files ${typescript ? '- `dist/` - Compiled output\n' : ''} - \`public/\` - Static assets - \`package.json\` - Project configuration ${typescript ? '- `tsconfig.json` - TypeScript configuration\n' : ''} ## Scripts - \`npm run dev\` - Start development server - \`npm run build\` - Build for production - \`npm start\` - Start production server `; } private async handleGenerateComponent(args: GenerateComponentArgs) { await this.validatePath(args.path); const componentContent = this.generateComponentContent(args); const fileName = `${args.name}.tsx`; const filePath = path.join(args.path, fileName); try { await fs.writeFile(filePath, componentContent); // Generate component documentation const docContent = this.generateComponentDocumentation(args); const docPath = path.join(args.path, `${args.name}.md`); await fs.writeFile(docPath, docContent); return { content: [ { type: 'text', text: `Component ${args.name} created successfully at ${filePath}`, }, ], }; } catch (error) { throw new McpError( ErrorCode.InternalError, `Failed to generate component: ${error instanceof Error ? error.message : String(error)}` ); } } private generateComponentContent(args: GenerateComponentArgs): string { const propsInterface = args.props ? `interface ${args.name}Props { ${Object.entries(args.props).map(([key, type]) => `${key}: ${type};`).join('\n ')} }` : ''; if (args.type === 'functional') { return `import React from 'react'; ${propsInterface} ${args.props ? `const ${args.name}: React.FC<${args.name}Props> = ({ ${Object.keys(args.props).join(', ')} }) => {` : `const ${args.name}: React.FC = () => {`} return ( <div> {/* Add your component content here */} </div> ); }; export default ${args.name}; `; } else { return `import React, { Component } from 'react'; ${propsInterface} class ${args.name} extends Component${args.props ? `<${args.name}Props>` : ''} { render() { return ( <div> {/* Add your component content here */} </div> ); } } export default ${args.name}; `; } } private generateComponentDocumentation(args: GenerateComponentArgs): string { return `# ${args.name} Component ## Overview ${args.type === 'functional' ? 'A functional React component' : 'A class-based React component'} ## Props ${args.props ? Object.entries(args.props) .map(([key, type]) => `- \`${key}\`: ${type}`) .join('\n') : 'This component does not accept any props.'} ## Usage \`\`\`tsx import ${args.name} from './${args.name}'; ${args.props ? `// Example usage with props <${args.name} ${Object.entries(args.props) .map(([key, type]) => `${key}={${this.getExampleValue(type)}}`) .join(' ')} />` : `// Example usage <${args.name} />`} \`\`\` `; } private getExampleValue(type: string): string { switch (type.toLowerCase()) { case 'string': return '"example"'; case 'number': return '42'; case 'boolean': return 'true'; case 'array': case 'string[]': return '["item1", "item2"]'; case 'object': return '{ key: "value" }'; default: return 'undefined'; } } private async handleCreateTypeDefinition(args: CreateTypeDefinitionArgs) { await this.validatePath(args.path); const typeContent = `export interface ${args.name} { ${Object.entries(args.properties) .map(([key, type]) => `${key}: ${type};`) .join('\n ')} } `; const filePath = path.join(args.path, `${args.name}.ts`); try { await fs.writeFile(filePath, typeContent); return { content: [ { type: 'text', text: `Type definition ${args.name} created successfully at ${filePath}`, }, ], }; } catch (error) { throw new McpError( ErrorCode.InternalError, `Failed to create type definition: ${error instanceof Error ? error.message : String(error)}` ); } } private async handleAddScript(args: AddScriptArgs) { await this.validatePath(args.path); try { const packageJsonPath = path.join(args.path, 'package.json'); const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf-8')); if (!packageJson.scripts) { packageJson.scripts = {}; } packageJson.scripts[args.name] = args.command; await fs.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2)); return { content: [ { type: 'text', text: `Added script '${args.name}': ${args.command}`, }, ], }; } catch (error) { throw new McpError( ErrorCode.InternalError, `Failed to add script: ${error instanceof Error ? error.message : String(error)}` ); } } private async handleUpdateTsConfig(args: UpdateTsConfigArgs) { await this.validatePath(args.path); try { const tsconfigPath = path.join(args.path, 'tsconfig.json'); interface TsConfig { compilerOptions: Record<string, unknown>; include?: string[]; exclude?: string[]; } let tsconfig: TsConfig = { compilerOptions: {} }; try { tsconfig = JSON.parse(await fs.readFile(tsconfigPath, 'utf-8')) as TsConfig; } catch { // Create new tsconfig if it doesn't exist tsconfig = { compilerOptions: {}, include: ["src/**/*"], exclude: ["node_modules", "dist"] }; } // Deep merge the new options tsconfig.compilerOptions = { ...tsconfig.compilerOptions, ...args.options, }; await fs.writeFile(tsconfigPath, JSON.stringify(tsconfig, null, 2)); return { content: [ { type: 'text', text: `Updated TypeScript configuration at ${tsconfigPath}`, }, ], }; } catch (error) { throw new McpError( ErrorCode.InternalError, `Failed to update TypeScript configuration: ${error instanceof Error ? error.message : String(error)}` ); } } private async handleInstallPackages(args: InstallPackageArgs) { await this.validatePath(args.path); try { // Verify package.json exists const packageJsonPath = path.join(args.path, 'package.json'); await fs.access(packageJsonPath); // Install packages const installCmd = `npm install ${args.dev ? '--save-dev' : ''} ${args.packages.join(' ')}`; const { stdout, stderr } = await execAsync(installCmd, { cwd: args.path }); return { content: [ { type: 'text', text: `Packages installed successfully:\n${stdout}\n${stderr}`, }, ], }; } catch (error) { throw new McpError( ErrorCode.InternalError, `Failed to install packages: ${error instanceof Error ? error.message : String(error)}` ); } } private async handleCreateDocumentation(args: CreateDocumentationArgs) { await this.validatePath(args.path); try { let content = ''; let fileName = ''; switch (args.type) { case 'readme': content = await this.generateProjectDocumentation(args.path); fileName = 'README.md'; break; case 'api': content = await this.generateApiDocumentation(args.path); fileName = 'API.md'; break; case 'component': if (!args.name) { throw new McpError(ErrorCode.InvalidParams, 'Component name is required for component documentation'); } content = await this.generateComponentDoc(args.path, args.name); fileName = `${args.name}.md`; break; } const docPath = path.join(args.path, fileName); await fs.writeFile(docPath, content); // Store in memory for resource access this.projectDocs.set(path.basename(args.path), content); return { content: [ { type: 'text', text: `Documentation created successfully at ${docPath}`, }, ], }; } catch (error) { throw new McpError( ErrorCode.InternalError, `Failed to create documentation: ${error instanceof Error ? error.message : String(error)}` ); } } private async generateProjectDocumentation(projectPath: string): Promise<string> { const packageJson = JSON.parse( await fs.readFile(path.join(projectPath, 'package.json'), 'utf-8') ); return `# ${packageJson.name} ## Description ${packageJson.description || 'A Node.js project'} ## Installation \`\`\`bash npm install \`\`\` ## Scripts ${Object.entries(packageJson.scripts || {}) .map(([name, command]) => `- \`npm run ${name}\`: ${command}`) .join('\n')} ## Dependencies ${Object.entries(packageJson.dependencies || {}) .map(([name, version]) => `- \`${name}\`: ${version}`) .join('\n')} ## Dev Dependencies ${Object.entries(packageJson.devDependencies || {}) .map(([name, version]) => `- \`${name}\`: ${version}`) .join('\n')} `; } private async generateApiDocumentation(projectPath: string): Promise<string> { // This is a basic implementation. In a real-world scenario, // you might want to use a tool like swagger-jsdoc to generate API documentation return `# API Documentation ## Endpoints ### GET /api Description of the GET /api endpoint ### POST /api Description of the POST /api endpoint ## Models Description of your data models ## Authentication Description of your authentication methods `; } private async generateComponentDoc(projectPath: string, componentName: string): Promise<string> { try { const componentPath = path.join(projectPath, `${componentName}.tsx`); const componentContent = await fs.readFile(componentPath, 'utf-8'); // Basic prop extraction - in a real implementation you might want to use // a proper TypeScript parser const propsMatch = componentContent.match(/interface (\w+)Props {([^}]+)}/); const props = propsMatch ? propsMatch[2].trim() : 'No props defined'; return `# ${componentName} Component ## Props \`\`\`typescript ${props} \`\`\` ## Usage \`\`\`tsx import ${componentName} from './${componentName}'; // Example usage <${componentName} /> \`\`\` ## Description Add component description here. `; } catch (error) { throw new McpError( ErrorCode.InternalError, `Failed to generate component documentation: ${error instanceof Error ? error.message : String(error)}` ); } } async run() { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error('Node.js Omnibus MCP server running on stdio'); } } const server = new NodeOmnibusServer(); server.run().catch(console.error);