UNPKG

openapi-mcp-generator

Version:

Generates MCP server code from OpenAPI specifications

203 lines 11.3 kB
#!/usr/bin/env node /** * OpenAPI to MCP Generator * * This tool generates a Model Context Protocol (MCP) server from an OpenAPI specification. * It creates a Node.js project that implements MCP over stdio to proxy API requests. */ import fs from 'fs/promises'; import path from 'path'; import { Command } from 'commander'; import SwaggerParser from '@apidevtools/swagger-parser'; // Import generators import { generateMcpServerCode, generatePackageJson, generateTsconfigJson, generateGitignore, generateEslintConfig, generateJestConfig, generatePrettierConfig, generateEnvExample, generateOAuth2Docs, generateWebServerCode, generateTestClientHtml, generateStreamableHttpCode, generateStreamableHttpClientHtml, } from './generator/index.js'; import pkg from '../package.json' with { type: 'json' }; // Export programmatic API export { getToolsFromOpenApi } from './api.js'; // Configure CLI const program = new Command(); program .name('openapi-mcp-generator') .description('Generates a buildable MCP server project (TypeScript) from an OpenAPI specification') .requiredOption('-i, --input <file_or_url>', 'Path or URL to the OpenAPI specification file (JSON or YAML)') .requiredOption('-o, --output <directory>', 'Path to the directory where the MCP server project will be created (e.g., ./petstore-mcp)') .option('-n, --server-name <n>', 'Name for the generated MCP server package (default: derived from OpenAPI info title)') .option('-v, --server-version <version>', 'Version for the generated MCP server (default: derived from OpenAPI info version or 0.1.0)') .option('-b, --base-url <url>', 'Base URL for the target API. Required if not specified in OpenAPI `servers` or if multiple servers exist.') .option('-t, --transport <type>', 'Server transport type: "stdio", "web", or "streamable-http" (default: "stdio")') .option('-p, --port <number>', 'Port for web or streamable-http transport (default: 3000)', (val) => parseInt(val, 10)) .option('--force', 'Overwrite existing files without prompting') .version(pkg.version) // Match package.json version .action((options) => { runGenerator(options).catch((error) => { console.error('Unhandled error:', error); process.exit(1); }); }); // Export the program object for use in bin stub export { program }; /** * Main function to run the generator */ async function runGenerator(options) { // Use the parsed options directly const outputDir = options.output; const inputSpec = options.input; const srcDir = path.join(outputDir, 'src'); const serverFilePath = path.join(srcDir, 'index.ts'); const packageJsonPath = path.join(outputDir, 'package.json'); const tsconfigPath = path.join(outputDir, 'tsconfig.json'); const gitignorePath = path.join(outputDir, '.gitignore'); const eslintPath = path.join(outputDir, '.eslintrc.json'); const prettierPath = path.join(outputDir, '.prettierrc'); const jestConfigPath = path.join(outputDir, 'jest.config.js'); const envExamplePath = path.join(outputDir, '.env.example'); const docsDir = path.join(outputDir, 'docs'); const oauth2DocsPath = path.join(docsDir, 'oauth2-configuration.md'); // Web server files (if requested) const webServerPath = path.join(srcDir, 'web-server.ts'); const publicDir = path.join(outputDir, 'public'); const indexHtmlPath = path.join(publicDir, 'index.html'); // StreamableHTTP files (if requested) const streamableHttpPath = path.join(srcDir, 'streamable-http.ts'); try { // Check if output directory exists and is not empty if (!options.force) { try { const dirExists = await fs.stat(outputDir).catch(() => false); if (dirExists) { const files = await fs.readdir(outputDir); if (files.length > 0) { console.error(`Error: Output directory ${outputDir} already exists and is not empty.`); console.error('Use --force to overwrite existing files.'); process.exit(1); } } } catch (err) { // Directory doesn't exist, which is fine } } // Parse OpenAPI spec console.error(`Parsing OpenAPI spec: ${inputSpec}`); const api = (await SwaggerParser.dereference(inputSpec)); console.error('OpenAPI spec parsed successfully.'); // Determine server name and version const serverNameRaw = options.serverName || api.info?.title || 'my-mcp-server'; const serverName = serverNameRaw.toLowerCase().replace(/[^a-z0-9_-]/g, '-'); const serverVersion = options.serverVersion || api.info?.version || '0.1.0'; console.error('Generating server code...'); const serverTsContent = generateMcpServerCode(api, options, serverName, serverVersion); console.error('Generating package.json...'); const packageJsonContent = generatePackageJson(serverName, serverVersion, options.transport); console.error('Generating tsconfig.json...'); const tsconfigJsonContent = generateTsconfigJson(); console.error('Generating .gitignore...'); const gitignoreContent = generateGitignore(); console.error('Generating ESLint config...'); const eslintConfigContent = generateEslintConfig(); console.error('Generating Prettier config...'); const prettierConfigContent = generatePrettierConfig(); console.error('Generating Jest config...'); const jestConfigContent = generateJestConfig(); console.error('Generating .env.example file...'); const envExampleContent = generateEnvExample(api.components?.securitySchemes); console.error('Generating OAuth2 documentation...'); const oauth2DocsContent = generateOAuth2Docs(api.components?.securitySchemes); console.error(`Creating project directory structure at: ${outputDir}`); await fs.mkdir(srcDir, { recursive: true }); await fs.writeFile(serverFilePath, serverTsContent); console.error(` -> Created ${serverFilePath}`); await fs.writeFile(packageJsonPath, packageJsonContent); console.error(` -> Created ${packageJsonPath}`); await fs.writeFile(tsconfigPath, tsconfigJsonContent); console.error(` -> Created ${tsconfigPath}`); await fs.writeFile(gitignorePath, gitignoreContent); console.error(` -> Created ${gitignorePath}`); await fs.writeFile(eslintPath, eslintConfigContent); console.error(` -> Created ${eslintPath}`); await fs.writeFile(prettierPath, prettierConfigContent); console.error(` -> Created ${prettierPath}`); await fs.writeFile(jestConfigPath, jestConfigContent); console.error(` -> Created ${jestConfigPath}`); await fs.writeFile(envExamplePath, envExampleContent); console.error(` -> Created ${envExamplePath}`); // Only write OAuth2 docs if there are OAuth2 security schemes if (oauth2DocsContent.includes('No OAuth2 security schemes defined')) { console.error(` -> No OAuth2 security schemes found, skipping documentation`); } else { await fs.mkdir(docsDir, { recursive: true }); await fs.writeFile(oauth2DocsPath, oauth2DocsContent); console.error(` -> Created ${oauth2DocsPath}`); } // Generate web server files if web transport is requested if (options.transport === 'web') { console.error('Generating web server files...'); // Generate web server code const webServerCode = generateWebServerCode(options.port || 3000); await fs.writeFile(webServerPath, webServerCode); console.error(` -> Created ${webServerPath}`); // Create public directory and index.html await fs.mkdir(publicDir, { recursive: true }); // Generate test client const indexHtmlContent = generateTestClientHtml(serverName); await fs.writeFile(indexHtmlPath, indexHtmlContent); console.error(` -> Created ${indexHtmlPath}`); } // Generate streamable HTTP files if streamable-http transport is requested if (options.transport === 'streamable-http') { console.error('Generating StreamableHTTP server files...'); // Generate StreamableHTTP server code const streamableHttpCode = generateStreamableHttpCode(options.port || 3000); await fs.writeFile(streamableHttpPath, streamableHttpCode); console.error(` -> Created ${streamableHttpPath}`); // Create public directory and index.html await fs.mkdir(publicDir, { recursive: true }); // Generate test client const indexHtmlContent = generateStreamableHttpClientHtml(serverName); await fs.writeFile(indexHtmlPath, indexHtmlContent); console.error(` -> Created ${indexHtmlPath}`); } console.error('\n---'); console.error(`MCP server project '${serverName}' successfully generated at: ${outputDir}`); console.error('\nNext steps:'); console.error(`1. Navigate to the directory: cd ${outputDir}`); console.error(`2. Install dependencies: npm install`); if (options.transport === 'web') { console.error(`3. Build the TypeScript code: npm run build`); console.error(`4. Run the server in web mode: npm run start:web`); console.error(` (This will start a web server on port ${options.port || 3000})`); console.error(` Access the test client at: http://localhost:${options.port || 3000}`); } else if (options.transport === 'streamable-http') { console.error(`3. Build the TypeScript code: npm run build`); console.error(`4. Run the server in StreamableHTTP mode: npm run start:http`); console.error(` (This will start a StreamableHTTP server on port ${options.port || 3000})`); console.error(` Access the test client at: http://localhost:${options.port || 3000}`); } else { console.error(`3. Build the TypeScript code: npm run build`); console.error(`4. Run the server: npm start`); console.error(` (This runs the built JavaScript code in build/index.js)`); } console.error('---'); } catch (error) { console.error('\nError generating MCP server project:', error); // Only attempt cleanup if the directory exists and force option was used if (options.force) { try { await fs.rm(outputDir, { recursive: true, force: true }); console.error(`Cleaned up partially created directory: ${outputDir}`); } catch (cleanupError) { console.error(`Failed to cleanup directory ${outputDir}:`, cleanupError); } } process.exit(1); } } // Export the run function for programmatic usage export { runGenerator as generateMcpServer }; //# sourceMappingURL=index.js.map