Add build folder
This commit is contained in:
parent
3b4f716661
commit
04cd82fa53
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,3 +1,2 @@
|
||||
node_modules
|
||||
.env
|
||||
build
|
||||
.env
|
||||
590
build/generator.js
Normal file
590
build/generator.js
Normal 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
82
build/index.js
Executable 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
35
build/utils.js
Normal 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;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user