Add build folder

This commit is contained in:
harsha-iiiv 2025-04-12 17:24:15 +05:30
parent 3b4f716661
commit 04cd82fa53
4 changed files with 708 additions and 2 deletions

3
.gitignore vendored
View File

@ -1,3 +1,2 @@
node_modules
.env
build
.env

590
build/generator.js Normal file
View File

@ -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<string, any>;
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<CallToolResult> => {
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<string, any> = {};\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<string, string> = { '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.';
}

82
build/index.js Executable file
View File

@ -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 <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 <name>', '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.')
.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();

35
build/utils.js Normal file
View File

@ -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;
}