This commit is contained in:
Harshavardhan Surisetty 2025-03-09 14:09:24 +05:30 committed by GitHub
commit 335643b45b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 1606 additions and 0 deletions

126
README.md Normal file
View File

@ -0,0 +1,126 @@
# OpenAPI to MCP
A command-line tool that generates Model Context Protocol (MCP) server code from OpenAPI specifications. This tool helps you quickly create an MCP server that acts as a bridge between LLMs (Large Language Models) and your API.
## Features
- **Automatic Tool Generation**: Converts each API endpoint in your OpenAPI spec into an MCP tool
- **Multiple Transport Options**: Supports stdio, WebSocket, and HTTP transport methods
- **Complete Project Setup**: Generates all necessary files to run an MCP server
- **TypeScript Support**: Includes TypeScript definitions and configuration
- **Easy Configuration**: Simple environment-based configuration for the generated server
## Installation
```bash
# Clone this repository
git clone https://github.com/yourusername/openapi-to-mcp.git
# Navigate to the project directory
cd openapi-to-mcp
# Install dependencies
npm install
# Make the script executable
chmod +x index.js
# Optionally, install globally
npm install -g .
```
## Usage
Generate an MCP server from an OpenAPI specification:
```bash
./index.js --openapi path/to/openapi.json --output ./my-mcp-server
```
Or if installed globally:
```bash
openapi-to-mcp --openapi path/to/openapi.json --output ./my-mcp-server
```
### Command Line Options
| Option | Alias | Description | Default |
|--------|-------|-------------|---------|
| `--openapi` | `-o` | Path or URL to OpenAPI specification | (required) |
| `--output` | `-d` | Output directory for generated files | `./mcp-server` |
| `--name` | `-n` | Name for the MCP server | `openapi-mcp-server` |
| `--version` | `-v` | Version for the MCP server | `1.0.0` |
| `--transport` | `-t` | Transport mechanism (stdio, websocket, http) | `stdio` |
| `--port` | `-p` | Port for websocket or HTTP server | `3000` |
| `--help` | `-h` | Show help information | |
### Examples
Generate from a local OpenAPI file:
```bash
./index.js --openapi ./specs/petstore.json --output ./petstore-mcp
```
Generate from a remote OpenAPI URL:
```bash
./index.js --openapi https://petstore3.swagger.io/api/v3/openapi.json --output ./petstore-mcp
```
Specify a WebSocket transport:
```bash
./index.js --openapi ./specs/petstore.json --transport websocket --port 8080
```
## Generated Files
The tool generates the following files in the output directory:
- `server.js` - The main MCP server implementation
- `package.json` - Dependencies and scripts
- `README.md` - Documentation for the generated server
- `.env.example` - Template for environment variables
- `types.d.ts` - TypeScript type definitions for the API
- `tsconfig.json` - TypeScript configuration
## Using the Generated Server
After generating your MCP server:
1. Navigate to the generated directory:
```bash
cd my-mcp-server
```
2. Install dependencies:
```bash
npm install
```
3. Create an environment file:
```bash
cp .env.example .env
```
4. Edit `.env` to set your API base URL and any required headers:
```
API_BASE_URL=https://api.example.com
API_HEADERS=Authorization:Bearer your-token-here
```
5. Start the server:
```bash
npm start
```
## Requirements
- Node.js 16.x or higher
- npm 7.x or higher
## License
MIT

30
package.json Normal file
View File

@ -0,0 +1,30 @@
{
"name": "openapi-mcp-generator",
"version": "1.0.0",
"description": "Generate MCP server code from OpenAPI specifications",
"type": "module",
"main": "src/index.js",
"bin": {
"openapi-mcp-generator": "./src/index.js"
},
"scripts": {
"start": "node src/index.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [
"openapi",
"mcp",
"model-context-protocol",
"generator",
"llm"
],
"author": "",
"license": "MIT",
"dependencies": {
"axios": "^1.6.0",
"minimist": "^1.2.8"
},
"engines": {
"node": ">=16.0.0"
}
}

323
src/config-generator.js Normal file
View File

@ -0,0 +1,323 @@
/**
* Generate .env.example with enhanced auth examples
*/
function generateEnvExample(config, securitySchemes) {
console.log('Generating .env.example file...');
let authExamples = '';
if (securitySchemes && Object.keys(securitySchemes).length > 0) {
authExamples = `# --- Authorization Configuration --- \n`;
for (const [schemeName, schemeDef] of Object.entries(securitySchemes)) {
if (schemeDef.type === 'apiKey') {
authExamples += `# Example for API Key "${schemeName}" (${schemeDef.in || 'header'}: ${schemeDef.name})\n`;
authExamples += `${schemeName.toUpperCase()}_${schemeDef.name.toUpperCase()}=YOUR_API_KEY_VALUE\n`;
} else if (schemeDef.type === 'http' && schemeDef.scheme === 'bearer') {
authExamples += `# Example for HTTP Bearer Token "${schemeName}"\n`;
authExamples += `${schemeName.toUpperCase()}_BEARERTOKEN=YOUR_BEARER_TOKEN_VALUE\n`;
} else if (schemeDef.type === 'http' && schemeDef.scheme === 'basic') {
authExamples += `# Example for HTTP Basic Auth "${schemeName}"\n`;
authExamples += `${schemeName.toUpperCase()}_USERNAME=YOUR_USERNAME\n`;
authExamples += `${schemeName.toUpperCase()}_PASSWORD=YOUR_PASSWORD\n`;
}
}
}
return `# API Configuration
API_BASE_URL=https://api.example.com
API_HEADERS= # (Less common now, use specific auth env vars below instead)
# Server Configuration
SERVER_NAME=${config.name}
SERVER_VERSION=${config.version}
TRANSPORT=stdio # Fixed to stdio
# Debug
DEBUG=false
${authExamples}
`;
}
/**
* Generate README.md with enhanced auth instructions
*/
function generateReadme(config, spec, tools, hasSecuritySchemes) {
console.log('Generating README.md file...');
const readme = `# ${config.name}
Model Context Protocol (MCP) server for ${spec.info?.title || 'OpenAPI'} API.
## Description
${spec.info?.description || 'This server provides a Model Context Protocol (MCP) interface to the API.'}
## Installation
1. Install dependencies:
\`\`\`bash
npm install
\`\`\`
2. Create a \`.env\` file based on \`.env.example\`:
\`\`\`bash
cp .env.example .env
\`\`\`
3. Edit the \`.env\` file to add your API configuration and authorization details.
## Configuration
The following environment variables can be configured in the \`.env\` file:
- \`API_BASE_URL\`: Base URL for the API (required)
- \`SERVER_NAME\`: Name of the MCP server (default: "${config.name}")
- \`SERVER_VERSION\`: Version of the MCP server (default: "${config.version}")
- \`DEBUG\`: Enable debug logging (true/false) (default: false)
${hasSecuritySchemes ? `
### Authorization Configuration
This server supports the following authorization schemes defined in the OpenAPI specification:
${Object.entries(spec.components?.securitySchemes || {}).map(([schemeName, schemeDef]) => {
let configDetails = '';
if (schemeDef.type === 'apiKey') {
configDetails = `- **${schemeName} (API Key)**: Set environment variable \`${schemeName.toUpperCase()}_${schemeDef.name.toUpperCase()}\` with your API key. The key will be sent in the \`${schemeDef.name}\` ${schemeDef.in || 'header'}.`;
} else if (schemeDef.type === 'http' && schemeDef.scheme === 'bearer') {
configDetails = `- **${schemeName} (HTTP Bearer)**: Set environment variable \`${schemeName.toUpperCase()}_BEARERTOKEN\` with your Bearer token. The token will be sent in the \`Authorization\` header.`;
} else if (schemeDef.type === 'http' && schemeDef.scheme === 'basic') {
configDetails = `- **${schemeName} (HTTP Basic)**: Set environment variables \`${schemeName.toUpperCase()}_USERNAME\` and \`${schemeName.toUpperCase()}_PASSWORD\` with your credentials. These will be encoded and sent in the \`Authorization\` header.`;
} else {
configDetails = `- **${schemeName} (${schemeDef.type})**: Configuration details for this scheme type are not fully described in this template. Refer to the OpenAPI specification and update \`.env.example\` and server code manually if needed.`;
}
return configDetails;
}).join('\n\n')}
`: ''}
## Usage
### Running the Server
The server is provided as both JavaScript and TypeScript versions:
\`\`\`bash
# Run JavaScript version
npm start
# Or run TypeScript version (compiles on the fly)
npm run start:ts
\`\`\`
### Building the TypeScript Version
\`\`\`bash
npm run build
cd dist
node server.js
\`\`\`
## Using as an MCP Tool Provider
This server implements the Model Context Protocol (MCP) and can be used with any MCP-compatible consumer, like Claude.js client or other MCP consumers.
Example of connecting to this server from a Claude.js client:
\`\`\`javascript
import { MCP } from "claude-js";
import { createStdio } from "claude-js/mcp";
// Create stdin/stdout transport
const transport = createStdio({ command: "node path/to/server.js" });
// Connect to the MCP server
const mcp = new MCP({ transport });
await mcp.connect();
// List available tools
const { tools } = await mcp.listTools();
console.log("Available tools:", tools);
// Call a tool
const result = await mcp.callTool({
id: "TOOL-ID",
arguments: { param1: "value1" }
});
console.log("Tool result:", result);
\`\`\`
## Available Tools
This MCP server provides the following tools:
${tools.map(tool => `### ${tool.name}
- **ID**: \`${tool.id}\`
- **Description**: ${tool.description || 'No description provided'}
- **Method**: \`${tool.method}\`
- **Path**: \`${tool.path}\`
${Object.keys(tool.inputSchema.properties).length > 0 ? '**Parameters**:\n\n' +
Object.entries(tool.inputSchema.properties).map(([name, prop]) =>
`- \`${name}\`: ${prop.description || name} ${tool.inputSchema.required?.includes(name) ? '(required)' : ''}`
).join('\n') : 'No parameters required.'}`).join('\n\n')}
## License
MIT
`;
return readme;
}
/**
* Generate package.json for the MCP server
*/
function generatePackageJson(config, spec) {
console.log('Generating package.json file...');
const packageJson = {
name: config.name.toLowerCase().replace(/\s+/g, '-'),
version: config.version,
description: `MCP server for ${spec.info?.title || 'OpenAPI'} API`,
type: 'module',
main: 'server.js',
scripts: {
start: 'node server.js',
build: 'node build.js',
"start:ts": "npx tsc && node dist/server.js"
},
dependencies: {
'@modelcontextprotocol/sdk': '^1.0.0',
'axios': '^1.6.0',
'dotenv': '^16.0.0',
},
devDependencies: {
'@types/node': '^20.11.0',
'typescript': '^5.3.3'
},
engines: {
'node': '>=16.0.0'
}
};
return JSON.stringify(packageJson, null, 2);
}
/**
* Generate a TypeScript declaration file
*/
function generateTypeDefinitions(tools) {
console.log('Generating types.d.ts file...');
return `/**
* Type definitions for the API endpoints
* Auto-generated from OpenAPI specification
*/
export interface APITools {
${tools.map(tool => ` /**
* ${tool.description || tool.name}
*/
"${tool.id}": {
params: {
${Object.entries(tool.inputSchema.properties).map(([name, prop]) =>
` /**
* ${prop.description || name}
*/
${name}${tool.inputSchema.required?.includes(name) ? '' : '?'}: ${prop.type === 'integer' ? 'number' : prop.type};`
).join('\n')}
};
response: any; // Response structure will depend on the API
};`).join('\n\n')}
}
`;
}
/**
* Generate the tsconfig.json file
*/
function generateTsConfig() {
console.log('Generating tsconfig.json file...');
return `{
"compilerOptions": {
"target": "es2020",
"module": "esnext",
"moduleResolution": "node",
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"outDir": "dist"
},
"include": ["*.ts", "*.js"],
"exclude": ["node_modules"]
}
`;
}
/**
* Generate build.js script for TypeScript compilation
*/
function generateBuildScript() {
console.log('Generating build.js file...');
return `#!/usr/bin/env node
import { exec } from 'child_process';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
// Get proper paths for ES modules
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Ensure dist directory exists
if (!fs.existsSync('./dist')) {
fs.mkdirSync('./dist');
}
// Run TypeScript compiler
console.log('Compiling TypeScript...');
exec('npx tsc', (error, stdout, stderr) => {
if (error) {
console.error('Error compiling TypeScript:', error);
console.error(stderr);
process.exit(1);
}
if (stdout) {
console.log(stdout);
}
console.log('TypeScript compilation successful');
// Copy .env.example to dist
try {
if (fs.existsSync('./.env.example')) {
fs.copyFileSync('./.env.example', './dist/.env.example');
console.log('Copied .env.example to dist directory');
}
// Create package.json in dist
const packageJson = JSON.parse(fs.readFileSync('./package.json', 'utf8'));
packageJson.main = 'server.js';
fs.writeFileSync('./dist/package.json', JSON.stringify(packageJson, null, 2));
console.log('Created package.json in dist directory');
console.log('Build completed successfully');
} catch (err) {
console.error('Error copying files:', err);
process.exit(1);
}
});
`;
}
export {
generatePackageJson,
generateReadme,
generateEnvExample,
generateTypeDefinitions,
generateTsConfig,
generateBuildScript
};

22
src/file-utils.js Normal file
View File

@ -0,0 +1,22 @@
import { writeFile } from 'fs/promises';
import path from 'path';
/**
* Copy the required MCP server template file if it doesn't exist in the output
*/
async function copyTemplateFile(file, content, outputDir, verbose = false) {
const outputPath = path.join(outputDir, file);
try {
await writeFile(outputPath, content);
console.log(`✓ Created ${outputPath}`);
if (verbose) {
console.log(` File size: ${content.length} bytes`);
}
return true;
} catch (error) {
console.error(`✗ Failed to create ${outputPath}: ${error.message}`);
throw error;
}
}
export { copyTemplateFile };

166
src/index.js Normal file
View File

@ -0,0 +1,166 @@
#!/usr/bin/env node
// Basic imports using ES module syntax
import minimist from 'minimist';
import fs from 'fs';
import { mkdir } from 'fs/promises';
import path from 'path';
import { fileURLToPath } from 'url';
import { loadOpenAPISpec } from './openapi-loader.js';
import { generateTools } from './tool-generator.js';
import { generateServerFile, generateServerTS } from './server-generator.js';
import {
generatePackageJson,
generateReadme,
generateEnvExample,
generateTypeDefinitions,
generateTsConfig,
generateBuildScript
} from './config-generator.js';
import { copyTemplateFile } from './file-utils.js';
// Get proper paths for ES modules
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
console.log('Script started');
/**
* Main function to drive the entire process
*/
async function main() {
try {
console.log('🚀 OpenAPI to MCP Server Generator');
// Parse command-line arguments with minimist
const argv = minimist(process.argv.slice(2), {
string: ['openapi', 'output', 'name', 'version', 'transport'],
number: ['port'],
alias: {
o: 'openapi',
d: 'output',
n: 'name',
v: 'version',
t: 'transport',
p: 'port',
V: 'verbose'
},
default: {
output: './mcp-server',
name: 'openapi-mcp-server',
version: '1.0.0',
transport: 'stdio',
port: 3000,
verbose: false
}
});
// Check required parameters
if (!argv.openapi) {
console.error('Error: --openapi parameter is required');
console.error('Usage: ./index.js --openapi <path-or-url> [--output <dir>]');
process.exit(1);
}
// Create configuration object
const config = {
openApiSpec: argv.openapi,
outputDir: argv.output,
name: argv.name,
version: argv.version,
transport: argv.transport,
port: argv.port,
verbose: argv.verbose
};
console.log(`Configuration:`);
console.log(`- OpenAPI Spec: ${config.openApiSpec}`);
console.log(`- Output Directory: ${config.outputDir}`);
console.log(`- Server Name: ${config.name}`);
console.log(`- Transport: ${config.transport}`);
// Load OpenAPI spec
const spec = await loadOpenAPISpec(config.openApiSpec, config.verbose);
if (!spec) {
throw new Error("Failed to load or parse the OpenAPI specification");
}
// Check if it's a valid OpenAPI spec
if (!spec.openapi && !spec.swagger) {
console.warn("Warning: The loaded specification might not be a valid OpenAPI document. Missing 'openapi' or 'swagger' version field.");
}
// Generate tools from spec
const { tools, toolMap, securitySchemes } = generateTools(spec, config.verbose);
const hasSecuritySchemes = Object.keys(securitySchemes).length > 0;
if (tools.length === 0) {
console.warn("Warning: No API tools were generated from the specification. The spec might not contain valid paths/operations.");
}
// Create output directory if it doesn't exist
if (!fs.existsSync(config.outputDir)) {
console.log(`Creating output directory: ${config.outputDir}`);
await mkdir(config.outputDir, { recursive: true });
}
// Generate all the files
console.log("Generating server files...");
const serverCode = generateServerFile(config, spec, toolMap, securitySchemes);
const serverTSCode = generateServerTS(config, spec, toolMap, securitySchemes);
const packageJson = generatePackageJson(config, spec);
const readme = generateReadme(config, spec, tools, hasSecuritySchemes);
const envExample = generateEnvExample(config, securitySchemes);
const typeDefinitions = generateTypeDefinitions(tools);
const tsConfig = generateTsConfig();
const buildScript = generateBuildScript();
// Write all files
console.log("Writing files to output directory...");
const results = await Promise.all([
copyTemplateFile('server.js', serverCode, config.outputDir, config.verbose),
copyTemplateFile('server.ts', serverTSCode, config.outputDir, config.verbose),
copyTemplateFile('package.json', packageJson, config.outputDir, config.verbose),
copyTemplateFile('README.md', readme, config.outputDir, config.verbose),
copyTemplateFile('.env.example', envExample, config.outputDir, config.verbose),
copyTemplateFile('types.d.ts', typeDefinitions, config.outputDir, config.verbose),
copyTemplateFile('tsconfig.json', tsConfig, config.outputDir, config.verbose),
copyTemplateFile('build.js', buildScript, config.outputDir, config.verbose)
]);
const success = results.every(Boolean);
if (success) {
console.log(`\n✅ MCP server generated successfully in "${config.outputDir}"`);
console.log(`📚 Generated ${tools.length} tools from OpenAPI spec`);
console.log('\nNext steps:');
console.log('1. cd ' + config.outputDir);
console.log('2. npm install');
console.log('3. cp .env.example .env (and edit with your API details)');
console.log('4. Run the server:');
console.log(' - JavaScript version: npm start');
console.log(' - TypeScript version: npm run start:ts');
} else {
console.error("❌ Some files failed to generate. Check the errors above.");
}
return success;
} catch (error) {
console.error('❌ Error generating MCP server:', error.message);
if (error.stack) {
console.error(error.stack);
}
return false;
}
}
// Run the program
main().then(success => {
process.exit(success ? 0 : 1);
}).catch(error => {
console.error('Unhandled error:', error);
process.exit(1);
});

49
src/openapi-loader.js Normal file
View File

@ -0,0 +1,49 @@
import fs from 'fs';
import { readFile } from 'fs/promises';
import path from 'path';
import axios from 'axios';
/**
* Load OpenAPI specification from file or URL
*/
async function loadOpenAPISpec(specPath, verbose = false) {
try {
if (specPath.startsWith('http')) {
// Load from URL
console.log(`Loading OpenAPI spec from URL: ${specPath}`);
const response = await axios.get(specPath);
if (verbose) {
console.log(`Successfully loaded OpenAPI spec from URL (${Object.keys(response.data).length} keys in spec)`);
}
return response.data;
} else {
// Load from local file
const resolvedPath = path.resolve(specPath);
console.log(`Loading OpenAPI spec from file: ${resolvedPath}`);
if (!fs.existsSync(resolvedPath)) {
throw new Error(`File not found: ${resolvedPath}`);
}
const content = await readFile(resolvedPath, 'utf-8');
try {
const parsed = JSON.parse(content);
if (verbose) {
console.log(`Successfully loaded OpenAPI spec from file (${Object.keys(parsed).length} keys in spec)`);
}
return parsed;
} catch (parseError) {
throw new Error(`Failed to parse JSON from ${resolvedPath}: ${parseError.message}`);
}
}
} catch (error) {
console.error(`Failed to load OpenAPI spec: ${error.message}`);
if (error.response) {
console.error(`HTTP Status: ${error.response.status}`);
console.error(`Response: ${JSON.stringify(error.response.data)}`);
}
throw error;
}
}
export { loadOpenAPISpec };

766
src/server-generator.js Normal file
View File

@ -0,0 +1,766 @@
/**
* Generate the main server.js file with fixes for MCP compatibility
*/
function generateServerFile(config, spec, toolMap, securitySchemes) {
console.log('Generating server.js file...');
const toolsArray = Object.values(toolMap);
const hasSecuritySchemes = Object.keys(securitySchemes).length > 0;
// Create JavaScript version with fixes for MCP compatibility
const serverCode = `#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import axios from "axios";
import { config as dotenvConfig } from "dotenv";
import {
ListToolsRequestSchema,
CallToolRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
// Load environment variables
dotenvConfig();
// Define tool schemas
const TOOLS = ${JSON.stringify(toolsArray, null, 2)};
const SECURITY_SCHEMES = ${JSON.stringify(securitySchemes, null, 2)};
/**
* MCP Server for ${spec.info?.title || 'OpenAPI'} API
* Generated from OpenAPI spec version ${spec.info?.version || 'unknown'}
* Generated on ${new Date().toISOString()}
*/
class MCPServer {
constructor() {
// Initialize class properties
this.server = null;
this.tools = new Map();
this.debug = process.env.DEBUG === "true";
this.baseUrl = process.env.API_BASE_URL || "";
this.headers = this.parseHeaders(process.env.API_HEADERS || "");
// Initialize tools map - do this before creating server
this.initializeTools();
// Create MCP server with correct capabilities
this.server = new Server(
{
name: process.env.SERVER_NAME || "${config.name}",
version: process.env.SERVER_VERSION || "${config.version}",
},
{
capabilities: {
tools: true, // Enable tools capability
},
}
);
// Set up request handlers - don't log here
this.setupHandlers();
}
/**
* Parse headers from string
*/
parseHeaders(headerStr) {
const headers = {};
if (headerStr) {
headerStr.split(",").forEach((header) => {
const [key, value] = header.split(":");
if (key && value) headers[key.trim()] = value.trim();
});
}
return headers;
}
/**
* Initialize tools map from OpenAPI spec
* This runs before the server is connected, so don't log here
*/
initializeTools() {
// Initialize each tool in the tools map
for (const tool of TOOLS) {
this.tools.set(tool.id, {
name: tool.name,
description: tool.description,
inputSchema: tool.inputSchema,
// Don't include security at the tool level
});
}
// Don't log here, we're not connected yet
console.error(\`Initialized \${this.tools.size} tools\`);
}
/**
* Set up request handlers
*/
setupHandlers() {
// Handle tool listing requests
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
this.log('debug', "Handling ListTools request");
// Return tools in the format expected by MCP SDK
return {
tools: Array.from(this.tools.entries()).map(([id, tool]) => ({
id,
...tool,
})),
};
});
// Handle tool execution requests
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { id, name, arguments: params } = request.params;
this.log('debug', "Handling CallTool request", { id, name, params });
let toolId;
let toolDetails;
try {
// Find the requested tool
toolId = id;
if (!toolId && name) {
for (const [tid, tool] of this.tools.entries()) {
if (tool.name === name) {
toolId = tid;
break;
}
}
}
if (!toolId) {
throw new Error(\`Tool not found: \${id || name}\`);
}
toolDetails = TOOLS.find(t => t.id === toolId);
if (!toolDetails) {
throw new Error(\`Tool details not found for ID: \${toolId}\`);
}
this.log('info', \`Executing tool: \${toolId}\`);
// Execute the API call
const result = await this.executeApiCall(toolDetails, params || {});
// Return the result in the correct MCP format
return {
content: [
{
type: "application/json",
data: result
}
]
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
this.log('error', \`Error executing tool \${toolId || name}: \${errorMessage}\`);
// Format error according to MCP SDK expectations
return {
error: {
message: errorMessage,
details: error.response?.data
? JSON.stringify(error.response.data)
: undefined
}
};
}
});
}
/**
* Execute an API call for a tool
*/
async executeApiCall(tool, params) {
// Get method and path from tool
const method = tool.method;
let path = tool.path;
// Clone params to avoid modifying the original
const requestParams = { ...params };
// Replace path parameters with values from params
Object.entries(requestParams).forEach(([key, value]) => {
const placeholder = \`{\${key}}\`;
if (path.includes(placeholder)) {
path = path.replace(placeholder, encodeURIComponent(String(value)));
delete requestParams[key]; // Remove used parameter
}
});
// Build the full URL
const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : \`\${this.baseUrl}/\`;
const cleanPath = path.startsWith("/") ? path.slice(1) : path;
const url = new URL(cleanPath, baseUrl).toString();
this.log('debug', \`API Request: \${method} \${url}\`);
try {
// Configure the request
const config = {
method: method.toLowerCase(),
url,
headers: { ...this.headers },
};
// Apply security headers based on tool security requirements
if (tool.security && Array.isArray(tool.security)) {
for (const requirement of tool.security) {
for (const securitySchemeName of Object.keys(requirement)) {
const securityDefinition = SECURITY_SCHEMES[securitySchemeName];
if (securityDefinition) {
const authType = securityDefinition.type;
// Handle API key
if (authType === 'apiKey') {
const apiKeyName = securityDefinition.name;
const envVarName = \`\${securitySchemeName.toUpperCase()}_\${apiKeyName.toUpperCase()}\`;
const apiKeyValue = process.env[envVarName];
if (apiKeyValue) {
if (securityDefinition.in === 'header') {
config.headers[apiKeyName] = apiKeyValue;
} else if (securityDefinition.in === 'query') {
config.params = config.params || {};
config.params[apiKeyName] = apiKeyValue;
}
} else {
this.log('warning', \`API Key environment variable not found: \${envVarName}\`);
}
}
// Handle bearer token
else if (authType === 'http' && securityDefinition.scheme === 'bearer') {
const envVarName = \`\${securitySchemeName.toUpperCase()}_BEARERTOKEN\`;
const bearerToken = process.env[envVarName];
if (bearerToken) {
config.headers['Authorization'] = \`Bearer \${bearerToken}\`;
} else {
this.log('warning', \`Bearer Token environment variable not found: \${envVarName}\`);
}
}
// Handle basic auth
else if (authType === 'http' && securityDefinition.scheme === 'basic') {
const username = process.env[\`\${securitySchemeName.toUpperCase()}_USERNAME\`];
const password = process.env[\`\${securitySchemeName.toUpperCase()}_PASSWORD\`];
if (username && password) {
const auth = Buffer.from(\`\${username}:\${password}\`).toString('base64');
config.headers['Authorization'] = \`Basic \${auth}\`;
} else {
this.log('warning', \`Basic auth credentials not found for \${securitySchemeName}\`);
}
}
}
}
}
}
// Add parameters based on request method
if (["GET", "DELETE"].includes(method)) {
// For GET/DELETE, send params as query string
config.params = { ...(config.params || {}), ...requestParams };
} else {
// For POST/PUT/PATCH, send params as JSON body
config.data = requestParams;
config.headers["Content-Type"] = "application/json";
}
this.log('debug', "Request config:", {
url: config.url,
method: config.method,
params: config.params,
headers: Object.keys(config.headers)
});
// Execute the request
const response = await axios(config);
this.log('debug', \`Response status: \${response.status}\`);
return response.data;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
this.log('error', \`API request failed: \${errorMessage}\`);
if (axios.isAxiosError(error)) {
const responseData = error.response?.data;
const responseStatus = error.response?.status;
this.log('error', 'API Error Details:', {
status: responseStatus,
data: typeof responseData === 'object' ? JSON.stringify(responseData) : responseData
});
// Rethrow with more context for better error handling
const detailedError = new Error(\`API request failed with status \${responseStatus}: \${errorMessage}\`);
detailedError.response = error.response;
throw detailedError;
}
throw error;
}
}
/**
* Log messages with appropriate level
* Only sends to MCP if we're connected
*/
log(level, message, data) {
// Always log to stderr for visibility
console.error(\`[\${level.toUpperCase()}] \${message}\${data ? ': ' + JSON.stringify(data) : ''}\`);
// Only try to send via MCP if we're in debug mode or it's important
if (this.debug || level !== 'debug') {
try {
// Only send if server exists and is connected
if (this.server && this.server.isConnected) {
this.server.sendLoggingMessage({
level,
data: \`[MCP Server] \${message}\${data ? ': ' + JSON.stringify(data) : ''}\`
});
}
} catch (e) {
// If logging fails, log to stderr
console.error('Failed to send log via MCP:', e.message);
}
}
}
/**
* Start the server
*/
async start() {
try {
// Create stdio transport
const transport = new StdioServerTransport();
console.error("MCP Server starting on stdio transport");
// Connect to the transport
await this.server.connect(transport);
// Now we can safely log via MCP
console.error(\`Registered \${this.tools.size} tools\`);
this.log('info', \`MCP Server started successfully with \${this.tools.size} tools\`);
} catch (error) {
console.error("Failed to start MCP server:", error);
process.exit(1);
}
}
}
// Start the server
async function main() {
try {
const server = new MCPServer();
await server.start();
} catch (error) {
console.error("Failed to start server:", error);
process.exit(1);
}
}
main();
`;
return serverCode;
}
/**
* Generate server.ts for TypeScript support with MCP compatibility fixes
*/
function generateServerTS(config, spec, toolMap, securitySchemes) {
console.log('Generating server.ts file...');
const toolsArray = Object.values(toolMap);
const hasSecuritySchemes = Object.keys(securitySchemes).length > 0;
const serverCode = `#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import axios, { AxiosRequestConfig, AxiosError } from "axios";
import { config as dotenvConfig } from "dotenv";
import {
ListToolsRequestSchema,
CallToolRequestSchema,
Tool,
JsonSchema,
} from "@modelcontextprotocol/sdk/types.js";
// Load environment variables
dotenvConfig();
// Define tool and security scheme types
interface OpenApiTool extends Tool {
method: string;
path: string;
security: any[];
}
interface SecurityScheme {
type: string;
name?: string;
in?: string;
scheme?: string;
}
// Define tool schemas
const TOOLS: OpenApiTool[] = ${JSON.stringify(toolsArray, null, 2)};
const SECURITY_SCHEMES: Record<string, SecurityScheme> = ${JSON.stringify(securitySchemes, null, 2)};
/**
* MCP Server for ${spec.info?.title || 'OpenAPI'} API
* Generated from OpenAPI spec version ${spec.info?.version || 'unknown'}
* Generated on ${new Date().toISOString()}
*/
class MCPServer {
private server: Server;
private tools: Map<string, Tool> = new Map();
private debug: boolean;
private baseUrl: string;
private headers: Record<string, string>;
constructor() {
// Initialize properties
this.debug = process.env.DEBUG === "true";
this.baseUrl = process.env.API_BASE_URL || "";
this.headers = this.parseHeaders(process.env.API_HEADERS || "");
// Initialize tools map - do this before creating server
this.initializeTools();
// Create MCP server with correct capabilities
this.server = new Server(
{
name: process.env.SERVER_NAME || "${config.name}",
version: process.env.SERVER_VERSION || "${config.version}",
},
{
capabilities: {
tools: true, // Enable tools capability
},
}
);
// Set up request handlers - don't log here
this.setupHandlers();
}
/**
* Parse headers from string
*/
private parseHeaders(headerStr: string): Record<string, string> {
const headers: Record<string, string> = {};
if (headerStr) {
headerStr.split(",").forEach((header) => {
const [key, value] = header.split(":");
if (key && value) headers[key.trim()] = value.trim();
});
}
return headers;
}
/**
* Initialize tools map from OpenAPI spec
* This runs before the server is connected, so don't log here
*/
private initializeTools(): void {
// Initialize each tool in the tools map
for (const tool of TOOLS) {
this.tools.set(tool.id, {
name: tool.name,
description: tool.description,
inputSchema: tool.inputSchema as JsonSchema,
// Don't include security at the tool level
});
}
// Don't log here, we're not connected yet
console.error(\`Initialized \${this.tools.size} tools\`);
}
/**
* Set up request handlers
*/
private setupHandlers(): void {
// Handle tool listing requests
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
this.log('debug', "Handling ListTools request");
// Return tools in the format expected by MCP SDK
return {
tools: Array.from(this.tools.entries()).map(([id, tool]) => ({
id,
...tool,
})),
};
});
// Handle tool execution requests
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { id, name, arguments: params } = request.params;
this.log('debug', "Handling CallTool request", { id, name, params });
let toolId: string | undefined;
let toolDetails: OpenApiTool | undefined;
try {
// Find the requested tool
toolId = id;
if (!toolId && name) {
for (const [tid, tool] of this.tools.entries()) {
if (tool.name === name) {
toolId = tid;
break;
}
}
}
if (!toolId) {
throw new Error(\`Tool not found: \${id || name}\`);
}
toolDetails = TOOLS.find(t => t.id === toolId);
if (!toolDetails) {
throw new Error(\`Tool details not found for ID: \${toolId}\`);
}
this.log('info', \`Executing tool: \${toolId}\`);
// Execute the API call
const result = await this.executeApiCall(toolDetails, params || {});
// Return the result in correct MCP format
return {
content: [
{
type: "application/json",
data: result
}
]
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
this.log('error', \`Error executing tool \${toolId || name}: \${errorMessage}\`);
// Format error according to MCP SDK expectations
return {
error: {
message: errorMessage,
details: error instanceof Error && 'response' in error
? JSON.stringify((error as any).response?.data)
: undefined
}
};
}
});
}
/**
* Execute an API call for a tool
*/
private async executeApiCall(tool: OpenApiTool, params: Record<string, any>): Promise<any> {
// Get method and path from tool
const method = tool.method;
let path = tool.path;
// Clone params to avoid modifying the original
const requestParams = { ...params };
// Replace path parameters with values from params
Object.entries(requestParams).forEach(([key, value]) => {
const placeholder = \`{\${key}}\`;
if (path.includes(placeholder)) {
path = path.replace(placeholder, encodeURIComponent(String(value)));
delete requestParams[key]; // Remove used parameter
}
});
// Build the full URL
const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : \`\${this.baseUrl}/\`;
const cleanPath = path.startsWith("/") ? path.slice(1) : path;
const url = new URL(cleanPath, baseUrl).toString();
this.log('debug', \`API Request: \${method} \${url}\`);
try {
// Configure the request
const config: AxiosRequestConfig = {
method: method.toLowerCase(),
url,
headers: { ...this.headers },
};
// Apply security headers based on tool security requirements
if (tool.security && Array.isArray(tool.security)) {
for (const requirement of tool.security) {
for (const securitySchemeName of Object.keys(requirement)) {
const securityDefinition = SECURITY_SCHEMES[securitySchemeName];
if (securityDefinition) {
const authType = securityDefinition.type;
// Handle API key
if (authType === 'apiKey') {
const apiKeyName = securityDefinition.name || '';
const envVarName = \`\${securitySchemeName.toUpperCase()}_\${apiKeyName.toUpperCase()}\`;
const apiKeyValue = process.env[envVarName];
if (apiKeyValue) {
if (securityDefinition.in === 'header') {
config.headers = config.headers || {};
config.headers[apiKeyName] = apiKeyValue;
} else if (securityDefinition.in === 'query') {
config.params = config.params || {};
config.params[apiKeyName] = apiKeyValue;
}
} else {
this.log('warning', \`API Key environment variable not found: \${envVarName}\`);
}
}
// Handle bearer token
else if (authType === 'http' && securityDefinition.scheme === 'bearer') {
const envVarName = \`\${securitySchemeName.toUpperCase()}_BEARERTOKEN\`;
const bearerToken = process.env[envVarName];
if (bearerToken) {
config.headers = config.headers || {};
config.headers['Authorization'] = \`Bearer \${bearerToken}\`;
} else {
this.log('warning', \`Bearer Token environment variable not found: \${envVarName}\`);
}
}
// Handle basic auth
else if (authType === 'http' && securityDefinition.scheme === 'basic') {
const username = process.env[\`\${securitySchemeName.toUpperCase()}_USERNAME\`];
const password = process.env[\`\${securitySchemeName.toUpperCase()}_PASSWORD\`];
if (username && password) {
const auth = Buffer.from(\`\${username}:\${password}\`).toString('base64');
config.headers = config.headers || {};
config.headers['Authorization'] = \`Basic \${auth}\`;
} else {
this.log('warning', \`Basic auth credentials not found for \${securitySchemeName}\`);
}
}
}
}
}
}
// Add parameters based on request method
if (["GET", "DELETE"].includes(method)) {
// For GET/DELETE, send params as query string
config.params = { ...(config.params || {}), ...requestParams };
} else {
// For POST/PUT/PATCH, send params as JSON body
config.data = requestParams;
if (config.headers) {
config.headers["Content-Type"] = "application/json";
}
}
this.log('debug', "Request config:", {
url: config.url,
method: config.method,
params: config.params,
headers: config.headers ? Object.keys(config.headers) : []
});
// Execute the request
const response = await axios(config);
this.log('debug', \`Response status: \${response.status}\`);
return response.data;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
this.log('error', \`API request failed: \${errorMessage}\`);
if (axios.isAxiosError(error)) {
const axiosError = error as AxiosError;
const responseData = axiosError.response?.data;
const responseStatus = axiosError.response?.status;
this.log('error', 'API Error Details:', {
status: responseStatus,
data: typeof responseData === 'object' ? JSON.stringify(responseData) : String(responseData)
});
// Rethrow with more context for better error handling
const detailedError = new Error(\`API request failed with status \${responseStatus}: \${errorMessage}\`);
(detailedError as any).response = axiosError.response;
throw detailedError;
}
throw error;
}
}
/**
* Log messages with appropriate level
* Only sends to MCP if we're connected
*/
private log(level: 'debug' | 'info' | 'warning' | 'error', message: string, data?: any): void {
// Always log to stderr for visibility
console.error(\`[\${level.toUpperCase()}] \${message}\${data ? ': ' + JSON.stringify(data) : ''}\`);
// Only try to send via MCP if we're in debug mode or it's important
if (this.debug || level !== 'debug') {
try {
// Only send if server exists and is connected
if (this.server && (this.server as any).isConnected) {
this.server.sendLoggingMessage({
level,
data: \`[MCP Server] \${message}\${data ? ': ' + JSON.stringify(data) : ''}\`
});
}
} catch (e) {
// If logging fails, log to stderr
console.error('Failed to send log via MCP:', (e as Error).message);
}
}
}
/**
* Start the server
*/
async start(): Promise<void> {
try {
// Create stdio transport
const transport = new StdioServerTransport();
console.error("MCP Server starting on stdio transport");
// Connect to the transport
await this.server.connect(transport);
// Now we can safely log via MCP
console.error(\`Registered \${this.tools.size} tools\`);
this.log('info', \`MCP Server started successfully with \${this.tools.size} tools\`);
} catch (error) {
console.error("Failed to start MCP server:", error);
process.exit(1);
}
}
}
// Start the server
async function main(): Promise<void> {
try {
const server = new MCPServer();
await server.start();
} catch (error) {
console.error("Failed to start server:", error);
process.exit(1);
}
}
main();
`;
return serverCode;
}
export { generateServerFile, generateServerTS };

124
src/tool-generator.js Normal file
View File

@ -0,0 +1,124 @@
/**
* Generate a clean tool ID from an API path and method
*/
function generateToolId(method, path) {
// Remove leading slash and parameters
const cleanPath = path.replace(/^\//, '').replace(/[{}]/g, '');
// Create a clean tool ID
return `${method.toUpperCase()}-${cleanPath}`.replace(/[^a-zA-Z0-9-]/g, '-');
}
/**
* Generate tool definitions from OpenAPI paths
*/
function generateTools(spec, verbose = false) {
const toolList = [];
const toolMapObj = {};
const securitySchemes = spec.components?.securitySchemes || {};
// Check if spec.paths exists
if (!spec.paths) {
console.warn("Warning: No paths found in OpenAPI specification");
return { tools: toolList, toolMap: toolMapObj, securitySchemes };
}
console.log(`Processing ${Object.keys(spec.paths).length} API paths...`);
for (const [path, pathItem] of Object.entries(spec.paths)) {
if (!pathItem) continue;
for (const [method, operation] of Object.entries(pathItem)) {
if (method === 'parameters' || !operation || typeof method !== 'string') continue;
// Skip if not a valid HTTP method
const validMethods = ['get', 'post', 'put', 'delete', 'patch', 'options', 'head'];
if (!validMethods.includes(method.toLowerCase())) continue;
const op = operation;
// Get a unique ID for this tool
const toolId = generateToolId(method, path);
// Create a friendly name
const toolName = op.operationId || op.summary || `${method.toUpperCase()} ${path}`;
if (verbose) {
console.log(`Processing endpoint: ${method.toUpperCase()} ${path} -> Tool ID: ${toolId}`);
}
const tool = {
id: toolId,
name: toolName,
description: op.description || `Make a ${method.toUpperCase()} request to ${path}`,
method: method.toUpperCase(),
path: path,
inputSchema: {
type: 'object',
properties: {},
required: []
},
security: op.security || spec.security || [] // Get security requirements for the operation or spec
};
// Add parameters from operation
if (op.parameters) {
for (const param of op.parameters) {
if ('name' in param && 'in' in param) {
const paramSchema = param.schema;
// Add parameter to the schema
tool.inputSchema.properties[param.name] = {
type: paramSchema?.type || 'string',
description: param.description || `${param.name} parameter`,
};
// Add enum values if present
if (paramSchema?.enum) {
tool.inputSchema.properties[param.name].enum = paramSchema.enum;
}
// Add required flag if needed
if (param.required) {
tool.inputSchema.required.push(param.name);
}
}
}
}
// Handle request body for POST/PUT/PATCH methods
if (['post', 'put', 'patch'].includes(method.toLowerCase()) && op.requestBody) {
const contentType = op.requestBody.content?.['application/json'];
if (contentType && contentType.schema) {
const bodySchema = contentType.schema;
// Add body properties to the tool's input schema
if (bodySchema.properties) {
for (const [propName, propSchema] of Object.entries(bodySchema.properties)) {
tool.inputSchema.properties[propName] = {
type: propSchema.type || 'string',
description: propSchema.description || `${propName} property`,
};
// Add enum values if present
if (propSchema.enum) {
tool.inputSchema.properties[propName].enum = propSchema.enum;
}
}
// Add required properties
if (bodySchema.required && Array.isArray(bodySchema.required)) {
tool.inputSchema.required.push(...bodySchema.required);
}
}
}
}
toolList.push(tool);
toolMapObj[toolId] = tool;
}
}
console.log(`Generated ${toolList.length} MCP tools from the OpenAPI spec`);
return { tools: toolList, toolMap: toolMapObj, securitySchemes }; // return securitySchemes as well
}
export { generateToolId, generateTools };