From 04cd82fa53aa8cd2c3b32185cc911044e71234ee Mon Sep 17 00:00:00 2001 From: harsha-iiiv Date: Sat, 12 Apr 2025 17:24:15 +0530 Subject: [PATCH] Add build folder --- .gitignore | 3 +- build/generator.js | 590 +++++++++++++++++++++++++++++++++++++++++++++ build/index.js | 82 +++++++ build/utils.js | 35 +++ 4 files changed, 708 insertions(+), 2 deletions(-) create mode 100644 build/generator.js create mode 100755 build/index.js create mode 100644 build/utils.js diff --git a/.gitignore b/.gitignore index ba81622..1dcef2d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,2 @@ node_modules -.env -build \ No newline at end of file +.env \ No newline at end of file diff --git a/build/generator.js b/build/generator.js new file mode 100644 index 0000000..fef8d28 --- /dev/null +++ b/build/generator.js @@ -0,0 +1,590 @@ +import { OpenAPIV3 } from 'openapi-types'; +import { generateOperationId } from './utils.js'; +/** + * Generates the TypeScript code content for the server's src/index.ts file. + */ +export function generateMcpServerCode(api, options, serverName, serverVersion) { + const tools = extractToolsFromApi(api); + const determinedBaseUrl = determineBaseUrl(api, options.baseUrl); + const listToolsCode = generateListTools(tools); + const callToolCode = generateCallTool(tools, determinedBaseUrl); + // --- Template for src/index.ts --- + return ` +// Generated by openapi-to-mcp-generator for ${serverName} v${serverVersion} +// Source OpenAPI spec: ${options.input} +// Generation date: ${new Date().toISOString()} + +import { Server } from "@modelcontextprotocol/sdk/server/index.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; + +// Import Schemas and Types from /types subpath with .js extension +import { + CallToolRequestSchema, + ListToolsRequestSchema, + type Tool, + type CallToolResult, + type CallToolRequest // Added type for the request parameter +} from "@modelcontextprotocol/sdk/types.js"; + +// Zod for runtime validation +import { z, ZodError } from 'zod'; +// Library to convert JSON Schema to Zod schema string at runtime +import { jsonSchemaToZod } from 'json-schema-to-zod'; + +// Define JsonObject locally as a utility type +type JsonObject = Record; + +import axios, { type AxiosRequestConfig, type AxiosError } from 'axios'; + +// --- Server Configuration --- +const SERVER_NAME = "${serverName}"; +const SERVER_VERSION = "${serverVersion}"; +const API_BASE_URL = "${determinedBaseUrl || ''}"; + +// --- Server Instance --- +const server = new Server( + { + name: SERVER_NAME, + version: SERVER_VERSION + }, + { + capabilities: { + tools: {} + } + } +); + +// --- Tool Definitions (for ListTools response) --- +// Corrected: Use Tool[] type +const toolsList: Tool[] = [ +${listToolsCode} +]; + +// --- Request Handlers --- + +// 1. List Available Tools Handler +server.setRequestHandler(ListToolsRequestSchema, async () => { + return { + tools: toolsList, + }; +}); + +// 2. Call Tool Handler +// Corrected: Added explicit type for 'request' parameter +server.setRequestHandler(CallToolRequestSchema, async (request: CallToolRequest): Promise => { + const { name: toolName, arguments: toolArgs } = request.params; + + const toolDefinition = toolsList.find(t => t.name === toolName); + + if (!toolDefinition) { + console.error(\`Error: Received request for unknown tool: \${toolName}\`); + return { content: [{ type: "text", text: \`Error: Unknown tool requested: \${toolName}\` }] }; + } + + // --- Tool Execution Logic --- +${callToolCode} // This generated code now includes Zod validation + + // Fallback error + console.error(\`Error: Handler logic missing for tool: \${toolName}. This indicates an issue in the generator.\`); + return { content: [{ type: "text", text: \`Error: Internal server error - handler not implemented for tool: \${toolName}\` }] }; +}); + + +// --- Main Execution Function --- +async function main() { + try { + const transport = new StdioServerTransport(); + await server.connect(transport); + console.error(\`\${SERVER_NAME} MCP Server (v\${SERVER_VERSION}) running on stdio\${API_BASE_URL ? \`, proxying API at \${API_BASE_URL}\` : ''}\`); + } catch (error) { + console.error("Error during server startup:", error); + process.exit(1); + } +} + +// --- Cleanup Function --- +async function cleanup() { + console.error("Shutting down MCP server..."); + process.exit(0); +} + +// Register signal handlers +process.on('SIGINT', cleanup); +process.on('SIGTERM', cleanup); + +// --- Start the Server --- +main().catch((error) => { + console.error("Fatal error in main execution:", error); + process.exit(1); +}); + +// --- Helper Functions (Included in the generated server code) --- +function formatApiError(error: AxiosError): string { + let message = 'API request failed.'; + if (error.response) { + message = \`API Error: Status \${error.response.status} (\${error.response.statusText || 'Status text not available'}). \`; + const responseData = error.response.data; + const MAX_LEN = 200; + if (typeof responseData === 'string') { + message += \`Response: \${responseData.substring(0, MAX_LEN)}\${responseData.length > MAX_LEN ? '...' : ''}\`; + } else if (responseData) { + try { + const jsonString = JSON.stringify(responseData); + message += \`Response: \${jsonString.substring(0, MAX_LEN)}\${jsonString.length > MAX_LEN ? '...' : ''}\`; + } catch { + message += 'Response: [Could not serialize response data]'; + } + } else { + message += 'No response body received.'; + } + } else if (error.request) { + message = 'API Network Error: No response received from the server. Check network connectivity or server availability.'; + if (error.code) message += \` (Code: \${error.code})\`; + } else { + message = \`API Request Setup Error: \${error.message}\`; + } + return message; +} + +/** + * Attempts to dynamically generate and evaluate a Zod schema from a JSON schema. + * WARNING: Uses eval(), which can be a security risk if the schema input is untrusted. + * In this context, the schema originates from the generator/OpenAPI spec, reducing risk. + * @param jsonSchema The JSON Schema object (or boolean). + * @param toolName For error logging. + * @returns The evaluated Zod schema object. + * @throws If schema conversion or evaluation fails. + */ +function getZodSchemaFromJsonSchema(jsonSchema: any, toolName: string): z.ZodTypeAny { + if (typeof jsonSchema !== 'object' || jsonSchema === null) { + // Handle boolean schemas or invalid input + console.warn(\`Cannot generate Zod schema for non-object JSON schema for tool '\${toolName}'. Input type: \${typeof jsonSchema}\`) + // Fallback to allowing any object - adjust if stricter handling is needed + return z.object({}).passthrough(); + } + try { + // Note: jsonSchemaToZod may require specific configurations or adjustments + // depending on the complexity of the JSON Schemas being converted. + const zodSchemaString = jsonSchemaToZod(jsonSchema); + + // IMPORTANT: Using eval() to execute the generated Zod schema string. + // This is generally discouraged due to security risks with untrusted input. + // Ensure the JSON schemas processed here are from trusted sources (like your OpenAPI spec). + // The 'z' variable (from imported zod) must be in scope for eval. + const zodSchema = eval(zodSchemaString); + + if (typeof zodSchema?.parse !== 'function') { + throw new Error('Generated Zod schema string did not evaluate to a valid Zod schema object.'); + } + return zodSchema as z.ZodTypeAny; + } catch (err: any) { + console.error(\`Failed to generate or evaluate Zod schema for tool '\${toolName}':\`, err); + // Fallback schema in case of conversion/evaluation error + // This allows any object, effectively skipping validation on error. + // Consider throwing the error if validation is critical. + return z.object({}).passthrough(); + } +} +`; +} +/** + * Generates the content for the package.json file for a buildable project. + * Adds zod and json-schema-to-zod dependencies. + */ +export function generatePackageJson(serverName, serverVersion) { + const packageData = { + name: serverName, + version: serverVersion, + description: `MCP Server generated from OpenAPI spec for ${serverName}`, + private: true, + type: "module", + main: "build/index.js", + files: ["build", "src"], + scripts: { + "start": "node build/index.js", + "build": "tsc && chmod 755 build/index.js", + "typecheck": "tsc --noEmit", + "prestart": "npm run build" + }, + engines: { + "node": ">=18.0.0" + }, + dependencies: { + "@modelcontextprotocol/sdk": "^1.9.0", + "axios": "^1.8.4", + "zod": "^3.24.2", + "json-schema-to-zod": "^2.6.1" + }, + devDependencies: { + "@types/node": "^18.19.0", + "typescript": "^5.4.5" + // Removed ts-node, tsc-watch + } + }; + return JSON.stringify(packageData, null, 2); +} +/** + * Generates the content for the tsconfig.json file for a buildable project. + * Enables stricter type checking. + */ +export function generateTsconfigJson() { + const tsconfigData = { + compilerOptions: { + "target": "ES2022", + "module": "Node16", + "moduleResolution": "Node16", + "outDir": "./build", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] + }; + return JSON.stringify(tsconfigData, null, 2); +} +/** + * Generates the content for the .gitignore file. + */ +export function generateGitignore() { + // Content unchanged from previous version + return ` +# Node dependencies +node_modules +# Build output +dist +build + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Coverage directory +coverage +*.lcov +.nyc_output + +# Build artifacts +.grunt +bower_components +# build/Release # Covered by build/ above +jspm_packages/ +web_modules/ +.lock-wscript + +# VS Code files +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Caches +.eslintcache +.node_repl_history +.browserslistcache + +# Environment variables +.env +.env.*.local +.env.local +`; +} +// --- Helper Functions below are mostly unchanged, except generateCallTool --- +function determineBaseUrl(api, cmdLineBaseUrl) { + // Logic unchanged + if (cmdLineBaseUrl) + return cmdLineBaseUrl.replace(/\/$/, ''); + if (api.servers && api.servers.length === 1 && api.servers[0].url) + return api.servers[0].url.replace(/\/$/, ''); + if (api.servers && api.servers.length > 1) { + console.warn(`⚠️ Multiple servers found. Using first: "${api.servers[0].url}". Use --base-url to override.`); + return api.servers[0].url.replace(/\/$/, ''); + } + return null; +} +function extractToolsFromApi(api) { + // Logic unchanged + const tools = []; + const usedNames = new Set(); + if (!api.paths) + return tools; + for (const [path, pathItem] of Object.entries(api.paths)) { + if (!pathItem) + continue; + for (const method of Object.values(OpenAPIV3.HttpMethods)) { + const operation = pathItem[method]; + if (!operation) + continue; + let baseName = operation.operationId || generateOperationId(method, path); + if (!baseName) { + console.warn(`⚠️ Skipping ${method.toUpperCase()} ${path}: missing operationId.`); + continue; + } + let finalToolName = baseName; + let counter = 1; + while (usedNames.has(finalToolName)) + finalToolName = `${baseName}_${counter++}`; + usedNames.add(finalToolName); + const description = operation.description || operation.summary || `Executes ${method.toUpperCase()} ${path}`; + const { inputSchema, parameters, requestBody } = generateInputSchema(operation); + tools.push({ name: finalToolName, description, inputSchema, operationId: baseName, method, path, parameters, requestBody }); + } + } + return tools; +} +function generateInputSchema(operation) { + // Logic unchanged + const properties = {}; + const required = []; + const allParameters = Array.isArray(operation.parameters) + ? operation.parameters.map(p => p) : []; + allParameters.forEach(param => { + if (!param.name || !param.schema) + return; + const paramSchema = mapOpenApiSchemaToJsonSchema(param.schema); + if (typeof paramSchema === 'object') + paramSchema.description = param.description || paramSchema.description; + properties[param.name] = paramSchema; + if (param.required) + required.push(param.name); + }); + let opRequestBody = undefined; + if (operation.requestBody) { + opRequestBody = operation.requestBody; + const jsonContent = opRequestBody.content?.['application/json']; + if (jsonContent?.schema) { + const bodySchema = mapOpenApiSchemaToJsonSchema(jsonContent.schema); + if (typeof bodySchema === 'object') + bodySchema.description = opRequestBody.description || bodySchema.description || 'The JSON request body.'; + properties['requestBody'] = bodySchema; + if (opRequestBody.required) + required.push('requestBody'); + } + else { + const firstContent = opRequestBody.content ? Object.entries(opRequestBody.content)[0] : undefined; + if (firstContent) { + const [contentType] = firstContent; + properties['requestBody'] = { type: 'string', description: opRequestBody.description || `Request body (content type: ${contentType})` }; + if (opRequestBody.required) + required.push('requestBody'); + } + } + } + const inputSchema = { type: 'object', properties, ...(required.length > 0 && { required }) }; + return { inputSchema, parameters: allParameters, requestBody: opRequestBody }; +} +function mapOpenApiSchemaToJsonSchema(schema) { + // Logic mostly unchanged, ensure it handles recursion correctly + if ('$ref' in schema) { + console.warn(`⚠️ Unresolved $ref '${schema.$ref}'. Schema may be incomplete.`); + return { type: 'object', description: `Unresolved: ${schema.$ref}` }; + } + if (typeof schema === 'boolean') + return schema; + const jsonSchema = { ...schema }; + if (schema.type === 'integer') + jsonSchema.type = 'number'; + delete jsonSchema.nullable; + delete jsonSchema.example; + delete jsonSchema.xml; + delete jsonSchema.externalDocs; + delete jsonSchema.deprecated; + delete jsonSchema.readOnly; + delete jsonSchema.writeOnly; + if (schema.nullable) { + if (Array.isArray(jsonSchema.type)) { + if (!jsonSchema.type.includes('null')) + jsonSchema.type.push('null'); + } + else if (typeof jsonSchema.type === 'string') + jsonSchema.type = [jsonSchema.type, 'null']; + else if (!jsonSchema.type) + jsonSchema.type = 'null'; + } + if (jsonSchema.type === 'object' && jsonSchema.properties) { + const mappedProps = {}; + for (const [key, propSchema] of Object.entries(jsonSchema.properties)) { + if (typeof propSchema === 'object' && propSchema !== null) + mappedProps[key] = mapOpenApiSchemaToJsonSchema(propSchema); + else if (typeof propSchema === 'boolean') + mappedProps[key] = propSchema; + } + jsonSchema.properties = mappedProps; + } + if (jsonSchema.type === 'array' && typeof jsonSchema.items === 'object' && jsonSchema.items !== null) { + jsonSchema.items = mapOpenApiSchemaToJsonSchema(jsonSchema.items); + } + return jsonSchema; +} +function generateListTools(tools) { + // Logic unchanged + if (tools.length === 0) + return " // No tools extracted from the OpenAPI spec."; + return tools.map(tool => { + const escapedDescription = (tool.description || '').replace(/\\/g, '\\\\').replace(/`/g, '\\`'); + let schemaString; + try { + schemaString = JSON.stringify(tool.inputSchema, null, 4).replace(/^/gm, ' '); + } + catch (e) { + schemaString = ' { "type": "object", "description": "Error: Could not stringify schema" }'; + } + return `\n // Tool: ${tool.name} (${tool.method.toUpperCase()} ${tool.path})\n {\n name: "${tool.name}",\n description: \`${escapedDescription}\`,\n inputSchema: ${schemaString}\n },`; + }).join(''); // Changed join to empty string to avoid extra newline +} +/** + * Generates the 'if/else if' block for the CallTool handler. + * Includes runtime Zod validation. + */ +function generateCallTool(tools, baseUrl) { + if (tools.length === 0) + return ' // No tools defined, so no handlers generated.'; + const cases = tools.map(tool => { + const { name, method, path: rawPath, parameters, requestBody } = tool; + const pathParams = parameters.filter(p => p.in === 'path'); + const queryParams = parameters.filter(p => p.in === 'query'); + const headerParams = parameters.filter(p => p.in === 'header'); + // --- Code Generation Snippets --- + // Zod validation block (remains the same) + const argsValidationCode = ` + // --- Argument Validation using Zod --- + let validatedArgs: JsonObject; + try { + const zodSchema = getZodSchemaFromJsonSchema(toolDefinition.inputSchema, toolName); + const argsToParse = (typeof toolArgs === 'object' && toolArgs !== null) ? toolArgs : {}; + validatedArgs = zodSchema.parse(argsToParse); + console.error(\`Arguments validated successfully for tool '\${toolName}'.\`); + } catch (error: any) { + if (error instanceof ZodError) { + const validationErrorMessage = \`Invalid arguments for tool '\${toolName}': \${error.errors.map(e => \`\${e.path.join('.')} (\${e.code}): \${e.message}\`).join(', ')}\`; + console.error(validationErrorMessage); + return { content: [{ type: 'text', text: validationErrorMessage }] }; + } else { + console.error(\`Unexpected error during argument validation setup for tool '\${toolName}':\`, error); + return { content: [{ type: 'text', text: \`Internal server error during argument validation setup for tool '\${toolName}'.\` }] }; + } + } + // --- End Argument Validation --- +`; + // URL Path Construction (uses validatedArgs) + let urlPathCode = ` let urlPath = "${rawPath}";\n`; + pathParams.forEach(p => { + urlPathCode += ` const ${p.name}_val = validatedArgs['${p.name}'];\n`; // Use distinct name to avoid clash + urlPathCode += ` if (typeof ${p.name}_val !== 'undefined' && ${p.name}_val !== null) { urlPath = urlPath.replace("{${p.name}}", encodeURIComponent(String(${p.name}_val))); }\n`; + }); + urlPathCode += ` if (urlPath.includes('{')) { throw new Error(\`Validation passed but failed to resolve path parameters in URL: \${urlPath}. Check schema/validation logic.\`); }\n`; + urlPathCode += ` const requestUrl = API_BASE_URL ? \`\${API_BASE_URL}\${urlPath}\` : urlPath;`; + // Query Parameters Construction (uses validatedArgs) + let queryParamsCode = ' const queryParams: Record = {};\n'; + queryParams.forEach(p => { + queryParamsCode += ` const ${p.name}_val = validatedArgs['${p.name}'];\n`; // Use distinct name + queryParamsCode += ` if (typeof ${p.name}_val !== 'undefined' && ${p.name}_val !== null) queryParams['${p.name}'] = ${p.name}_val;\n`; + }); + // Headers Construction (uses validatedArgs) + let headersCode = ` const headers: Record = { 'Accept': 'application/json' };\n`; + headerParams.forEach(p => { + headersCode += ` const ${p.name}_val = validatedArgs['${p.name}'];\n`; // Use distinct name + headersCode += ` if (typeof ${p.name}_val !== 'undefined' && ${p.name}_val !== null) headers['${p.name.toLowerCase()}'] = String(${p.name}_val);\n`; + }); + // **Corrected Request Body Handling** + let requestBodyDeclarationCode = ''; // Code to declare and assign requestBodyData + let axiosDataProperty = ''; // String part for the Axios config's 'data' property + let requestContentType = 'application/json'; // Default assumption + if (requestBody) { // Only generate body handling if the tool expects one + // Declare the variable *before* config construction + requestBodyDeclarationCode = ` let requestBodyData: any = undefined;\n`; + // Assign value *after* validation (which sets validatedArgs) + requestBodyDeclarationCode += ` if (validatedArgs && typeof validatedArgs['requestBody'] !== 'undefined') {\n`; + requestBodyDeclarationCode += ` requestBodyData = validatedArgs['requestBody'];\n`; + requestBodyDeclarationCode += ` }\n`; + // Determine Content-Type (must happen before headers are finalized in config) + if (requestBody.content?.['application/json']) { + requestContentType = 'application/json'; + } + else if (requestBody.content) { + const firstType = Object.keys(requestBody.content)[0]; + if (firstType) { + requestContentType = firstType; + } + } + // Add Content-Type header *if* data might exist + headersCode += ` // Set Content-Type based on OpenAPI spec (or fallback)\n`; + headersCode += ` if (typeof validatedArgs?.['requestBody'] !== 'undefined') { headers['content-type'] = '${requestContentType}'; }\n`; + // Set the string for the Axios config 'data' property + axiosDataProperty = 'data: requestBodyData, // Pass the prepared request body data'; + } + // --- Assemble the 'if' block for this tool --- + // Ensure correct order: Validation -> Declarations -> Config -> Axios Call + return ` + // Handler for tool: ${name} + if (toolName === "${name}") { + try { +${argsValidationCode} + // --- API Call Preparation --- +${urlPathCode} +${queryParamsCode} +${headersCode} +${requestBodyDeclarationCode} // Declare and assign requestBodyData *here* + + // --- Axios Request Configuration --- + // Now 'requestBodyData' is declared before being referenced here + const config: AxiosRequestConfig = { + method: "${method.toUpperCase()}", + url: requestUrl, + params: queryParams, + headers: headers, + ${axiosDataProperty} // Include data property conditionally + // Add Authentication logic here if needed + }; + + console.error(\`Executing tool "\${toolName}": \${config.method} \${config.url}\`); + + // --- Execute API Call --- + const response = await axios(config); + + // --- Process Successful Response --- + let responseText = ''; + const contentType = response.headers['content-type']?.toLowerCase() || ''; + if (contentType.includes('application/json') && typeof response.data === 'object' && response.data !== null) { + try { responseText = JSON.stringify(response.data, null, 2); } + catch (e) { responseText = "[Error: Failed to stringify JSON response]"; } + } else if (typeof response.data === 'string') { + responseText = response.data; + } else if (response.data !== undefined && response.data !== null) { + responseText = String(response.data); + } else { + responseText = \`(Status: \${response.status} - No body content)\`; + } + return { content: [ { type: "text", text: \`API Response (Status: \${response.status}):\\n\${responseText}\` } ], }; + + } catch (error: any) { + // --- Handle Errors (Post-Validation) --- + let errorMessage = \`Error executing tool '\${toolName}': \${error.message}\`; + if (axios.isAxiosError(error)) { errorMessage = formatApiError(error); } + else if (error instanceof Error) { errorMessage = error.message; } + else { errorMessage = 'An unexpected error occurred: ' + String(error); } + console.error(\`Error during execution of tool '\${toolName}':\`, errorMessage, error.stack); + return { content: [{ type: "text", text: errorMessage }] }; + } + }`; + }).join(' else '); + return cases || ' // No tools defined, so no handlers generated.'; +} diff --git a/build/index.js b/build/index.js new file mode 100755 index 0000000..21ee1c3 --- /dev/null +++ b/build/index.js @@ -0,0 +1,82 @@ +#!/usr/bin/env node +import { Command } from 'commander'; +import SwaggerParser from '@apidevtools/swagger-parser'; +import * as fs from 'fs/promises'; +import * as path from 'path'; +import { generateMcpServerCode, generatePackageJson, generateTsconfigJson, generateGitignore } from './generator.js'; +const program = new Command(); +program + .name('openapi-mcp-generator') + .description('Generates a buildable MCP server project (TypeScript) from an OpenAPI specification') + // Ensure these option definitions are robust + .requiredOption('-i, --input ', 'Path or URL to the OpenAPI specification file (JSON or YAML)') + .requiredOption('-o, --output ', 'Path to the directory where the MCP server project will be created (e.g., ./petstore-mcp)') + .option('-n, --server-name ', 'Name for the generated MCP server package (default: derived from OpenAPI info title)') + .option('-v, --server-version ', 'Version for the generated MCP server (default: derived from OpenAPI info version or 0.1.0)') + .option('-b, --base-url ', 'Base URL for the target API. Required if not specified in OpenAPI `servers` or if multiple servers exist.') + .version('2.0.0'); // Match package.json version +// Parse arguments explicitly from process.argv +// This is generally the most reliable way +program.parse(process.argv); +// Retrieve the options AFTER parsing +const options = program.opts(); +async function main() { + // Use the parsed options directly + const outputDir = options.output; + const inputSpec = options.input; // Use the parsed input value + 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'); + try { + // Use the correct inputSpec variable + console.error(`Parsing OpenAPI spec: ${inputSpec}`); + const api = await SwaggerParser.dereference(inputSpec); + console.error('OpenAPI spec parsed successfully.'); + // Use options directly for name/version/baseUrl determination + 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...'); + // Pass inputSpec to generator function if needed for comments, otherwise just options + const serverTsContent = generateMcpServerCode(api, options, serverName, serverVersion); + console.error('Generating package.json...'); + const packageJsonContent = generatePackageJson(serverName, serverVersion); + console.error('Generating tsconfig.json...'); + const tsconfigJsonContent = generateTsconfigJson(); + console.error('Generating .gitignore...'); + const gitignoreContent = generateGitignore(); + 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}`); + 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`); + 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); + 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); + } +} +main(); diff --git a/build/utils.js b/build/utils.js new file mode 100644 index 0000000..a48f5b1 --- /dev/null +++ b/build/utils.js @@ -0,0 +1,35 @@ +export function titleCase(str) { + // Converts snake_case, kebab-case, or path/parts to TitleCase + return str + .toLowerCase() + .replace(/[-_\/](.)/g, (_, char) => char.toUpperCase()) // Handle separators + .replace(/^{/, '') // Remove leading { from path params + .replace(/}$/, '') // Remove trailing } from path params + .replace(/^./, (char) => char.toUpperCase()); // Capitalize first letter +} +export function generateOperationId(method, path) { + // Generator: get /users/{userId}/posts -> GetUsersPostsByUserId + const parts = path.split('/').filter(p => p); // Split and remove empty parts + let name = method.toLowerCase(); // Start with method name + parts.forEach((part, index) => { + if (part.startsWith('{') && part.endsWith('}')) { + // Append 'By' + ParamName only for the *last* path parameter segment + if (index === parts.length - 1) { + name += 'By' + titleCase(part); + } + // Potentially include non-terminal params differently if needed, e.g.: + // else { name += 'With' + titleCase(part); } + } + else { + // Append the static path part in TitleCase + name += titleCase(part); + } + }); + // Simple fallback if name is just the method (e.g., GET /) + if (name === method.toLowerCase()) { + name += 'Root'; + } + // Ensure first letter is uppercase after potential lowercase method start + name = name.charAt(0).toUpperCase() + name.slice(1); + return name; +}