Refactor OpenAPI MCP Server Generator to TypeScript

- Removed the old JavaScript implementation of the MCP server generator.
- Introduced a new TypeScript implementation with improved command-line argument parsing using `commander`.
- Replaced the OpenAPI spec loading mechanism with `@apidevtools/swagger-parser` for better handling of references.
- Updated the server generation logic to create TypeScript files with appropriate typings and structure.
- Added utility functions for generating operation IDs and converting strings to TitleCase.
- Created a new `tsconfig.json` for TypeScript compilation settings.
- Removed deprecated files related to the old JavaScript implementation, including `openapi-loader.js`, `server-generator.js`, and `tool-generator.js`.
- Enhanced error handling and logging throughout the new implementation.
This commit is contained in:
harsha-iiiv 2025-04-12 17:21:28 +05:30
parent 2c4f2e4f49
commit 3b4f716661
17 changed files with 3224 additions and 1554 deletions

3
.gitignore vendored Normal file
View File

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

138
README.md
View File

@ -1,17 +1,18 @@
# OpenAPI to MCP Generator
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.
# OpenAPI to MCP Generator (openapi-mcp-generator)
[![npm version](https://img.shields.io/npm/v/openapi-mcp-generator.svg)](https://www.npmjs.com/package/openapi-mcp-generator)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![GitHub repository](https://img.shields.io/badge/GitHub-harsha--iiiv/openapi--mcp--generator-blue.svg)](https://github.com/harsha-iiiv/openapi-mcp-generator)
A command-line tool that generates a complete, buildable Model Context Protocol (MCP) server project in TypeScript from an OpenAPI specification. This tool helps you quickly create an MCP server using **stdio transport** 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
- **Automatic Tool Generation**: Converts each API operation in your OpenAPI spec into an MCP tool within the generated server.
- **Complete Project Setup**: Generates a full Node.js project structure (`package.json`, `tsconfig.json`, `src/index.ts`, `.gitignore`).
- **TypeScript & Build Ready**: Creates a TypeScript project configured for building into JavaScript for execution.
- **Stdio Transport**: Generated server uses standard input/output for communication with the MCP client (e.g., an LLM).
- **Runtime Argument Validation**: Integrates Zod for validating tool arguments at runtime based on the OpenAPI schema.
## Installation
@ -28,100 +29,101 @@ pnpm add -g openapi-mcp-generator
## Usage
Generate an MCP server from an OpenAPI specification:
Generate an MCP server project from an OpenAPI specification:
```bash
openapi-mcp-generator --openapi path/to/openapi.json --output /Path/to/output
openapi-mcp-generator -i <path_or_url_to_openapi> -o <output_directory_path> [options]
```
### 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 | |
| Option | Alias | Description | Default |
| :--------------- | :---- | :-------------------------------------------------------------------------------------------------- | :------------------------------------------- |
| `--input` | `-i` | Path or URL to the OpenAPI specification file (JSON or YAML). | **(Required)** |
| `--output` | `-o` | Path to the directory where the MCP server project will be created. | **(Required)** |
| `--name` | `-n` | Name for the generated MCP server package (`package.json`). | Derived from OpenAPI title or `my-mcp-server` |
| `--version` | `-v` | Version for the generated MCP server package (`package.json`). | Derived from OpenAPI version or `0.1.0` |
| `--base-url` | `-b` | Base URL for the target API. Required if not in OpenAPI `servers` or if multiple servers specified. | Derived from OpenAPI `servers` if possible |
| `--help` | `-h` | Show help information. | |
### Examples
Generate from a local OpenAPI file:
**Generate from a local OpenAPI file:**
```bash
openapi-mcp-generator --openapi ./specs/petstore.json --output ./petstore-mcp
openapi-mcp-generator -i ./specs/my-api.yaml -o ./my-api-mcp-server
```
Generate from a remote OpenAPI URL:
**Generate from a remote OpenAPI URL, specifying name and base URL:**
```bash
openapi-mcp-generator --openapi https://petstore3.swagger.io/api/v3/openapi.json --output ./petstore-mcp
openapi-mcp-generator \
-i https://petstore3.swagger.io/api/v3/openapi.json \
-o ./petstore-mcp \
-n petstore-server \
-b https://petstore3.swagger.io/api/v3
```
Specify a WebSocket transport:
## Generated Project Structure
The tool generates the following structure in the specified output directory:
```bash
openapi-mcp-generator --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
<output_directory>/
├── .gitignore # Standard Node.js gitignore
├── package.json # Dependencies and scripts for the generated server
├── tsconfig.json # TypeScript configuration for building
└── src/
└── index.ts # The generated MCP server source code
```
## Using the Generated Server
After generating your MCP server:
After generating your MCP server project:
1. Navigate to the generated directory:
```bash
cd my-mcp-server
```
1. **Navigate to the generated directory:**
```bash
cd <output_directory>
# e.g., cd ./petstore-mcp
```
2. Install dependencies:
```bash
npm install
```
2. **Install dependencies:**
```bash
npm install
```
This installs `@modelcontextprotocol/sdk`, `axios`, `zod`, `json-schema-to-zod`, and `typescript`.
3. Create an environment file:
```bash
cp .env.example .env
```
3. **(Optional) Implement Authentication/Configuration:**
* The generator sets the `API_BASE_URL` constant in `src/index.ts` if provided or derived.
* You will need to **manually edit `src/index.ts`** to add any necessary API authentication (e.g., setting `Authorization` headers in the `AxiosRequestConfig` using environment variables or other secure methods). Look for the `// Add Authentication logic here if needed` comment within the `CallTool` handler.
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
```
4. **Build the server code:**
```bash
npm run build
```
This compiles the TypeScript code from `src/` into JavaScript in the `build/` directory.
5. Start the server:
```bash
npm start
```
5. **Start the server:**
```bash
npm start
```
This runs the compiled JavaScript server (`node build/index.js`), which will listen for MCP requests on standard input/output.
## Requirements
- Node.js 16.x or higher
- npm 7.x or higher
- Node.js v18.0.0 or higher (for both the generator and the generated server)
## Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
Contributions are welcome! Please feel free to submit a Pull Request to the [GitHub repository](https://github.com/harsha-iiiv/openapi-mcp-generator).
1. Fork the repository
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
4. Push to the branch (`git push origin feature/amazing-feature`)
5. Open a Pull Request
1. Fork the repository
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
4. Push to the branch (`git push origin feature/amazing-feature`)
5. Open a Pull Request
## License
MIT
[MIT](https://opensource.org/licenses/MIT)
```

55
examples/petstore-mcp/.gitignore vendored Normal file
View File

@ -0,0 +1,55 @@
# 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

View File

@ -0,0 +1,31 @@
{
"name": "swagger-petstore---openapi-3-0",
"version": "1.0.26",
"description": "MCP Server generated from OpenAPI spec for swagger-petstore---openapi-3-0",
"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",
"json-schema-to-zod": "^2.6.1",
"zod": "^3.24.2"
},
"devDependencies": {
"@types/node": "^18.19.0",
"typescript": "^5.4.5"
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,27 @@
{
"compilerOptions": {
"esModuleInterop": true,
"skipLibCheck": true,
"target": "ES2022",
"allowJs": true,
"resolveJsonModule": true,
"moduleDetection": "force",
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"module": "NodeNext",
"moduleResolution": "NodeNext",
"noEmit": false,
"outDir": "./build",
"declaration": true,
"sourceMap": true,
"forceConsistentCasingInFileNames": true
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"build"
]
}

View File

@ -1,38 +1,50 @@
{
"name": "openapi-mcp-generator",
"version": "1.0.4",
"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": "Harsha",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/harsha-iiiv/openapi-mcp-generator.git"
},
"bugs": {
"url": "https://github.com/harsha-iiiv/openapi-mcp-generator/issues"
},
"homepage": "https://github.com/harsha-iiiv/openapi-mcp-generator#readme",
"dependencies": {
"axios": "^1.6.0",
"minimist": "^1.2.8"
},
"engines": {
"node": ">=16.0.0"
}
"name": "openapi-mcp-generator",
"version": "2.0.0",
"description": "Generates MCP server code from OpenAPI specifications",
"license": "MIT",
"author": "Harsha",
"type": "module",
"engines": {
"node": ">=18.0.0"
},
"bin": {
"openapi-mcp-generator": "./build/index.js"
},
"files": [
"build"
],
"scripts": {
"start": "node build/index.js",
"typecheck": "tsc --noEmit",
"build": "tsc && chmod 755 build/index.js"
},
"keywords": [
"openapi",
"mcp",
"model-context-protocol",
"generator",
"llm"
],
"repository": {
"type": "git",
"url": "git+https://github.com/harsha-iiiv/openapi-mcp-generator.git"
},
"bugs": {
"url": "https://github.com/harsha-iiiv/openapi-mcp-generator/issues"
},
"homepage": "https://github.com/harsha-iiiv/openapi-mcp-generator#readme",
"dependencies": {
"@apidevtools/swagger-parser": "^10.1.1",
"@modelcontextprotocol/sdk": "^1.9.0",
"axios": "^1.8.4",
"commander": "^13.1.0",
"zod": "^3.24.2"
},
"devDependencies": {
"typescript": "^5.8.3"
},
"peerDependencies": {
"@modelcontextprotocol/sdk": "^1.9.0"
}
}

View File

@ -1,323 +0,0 @@
/**
* 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
};

View File

@ -1,22 +0,0 @@
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 };

604
src/generator.ts Normal file
View File

@ -0,0 +1,604 @@
import { OpenAPIV3 } from 'openapi-types';
import type { JSONSchema7, JSONSchema7TypeName } from 'json-schema';
import { generateOperationId } from './utils.js';
interface CliOptions {
input: string;
output: string; // This is the directory path
serverName?: string;
serverVersion?: string;
baseUrl?: string;
}
interface McpToolDefinition {
name: string;
description: string;
inputSchema: JSONSchema7 | boolean;
operationId: string;
method: string;
path: string;
parameters: OpenAPIV3.ParameterObject[];
requestBody?: OpenAPIV3.RequestBodyObject;
}
/**
* Generates the TypeScript code content for the server's src/index.ts file.
*/
export function generateMcpServerCode(
api: OpenAPIV3.Document,
options: CliOptions,
serverName: string,
serverVersion: string
): string {
const tools: McpToolDefinition[] = 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: string, serverVersion: string): string {
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(): string {
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(): string {
// 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: OpenAPIV3.Document, cmdLineBaseUrl?: string): string | null {
// 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: OpenAPIV3.Document): McpToolDefinition[] {
// Logic unchanged
const tools: McpToolDefinition[] = [];
const usedNames = new Set<string>();
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: OpenAPIV3.OperationObject): { inputSchema: JSONSchema7 | boolean, parameters: OpenAPIV3.ParameterObject[], requestBody?: OpenAPIV3.RequestBodyObject } {
// Logic unchanged
const properties: { [key: string]: JSONSchema7 | boolean } = {};
const required: string[] = [];
const allParameters: OpenAPIV3.ParameterObject[] = Array.isArray(operation.parameters)
? operation.parameters.map(p => p as OpenAPIV3.ParameterObject) : [];
allParameters.forEach(param => {
if (!param.name || !param.schema) return;
const paramSchema = mapOpenApiSchemaToJsonSchema(param.schema as OpenAPIV3.SchemaObject);
if (typeof paramSchema === 'object') paramSchema.description = param.description || paramSchema.description;
properties[param.name] = paramSchema;
if (param.required) required.push(param.name);
});
let opRequestBody: OpenAPIV3.RequestBodyObject | undefined = undefined;
if (operation.requestBody) {
opRequestBody = operation.requestBody as OpenAPIV3.RequestBodyObject;
const jsonContent = opRequestBody.content?.['application/json'];
if (jsonContent?.schema) {
const bodySchema = mapOpenApiSchemaToJsonSchema(jsonContent.schema as OpenAPIV3.SchemaObject);
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: JSONSchema7 = { type: 'object', properties, ...(required.length > 0 && { required }) };
return { inputSchema, parameters: allParameters, requestBody: opRequestBody };
}
function mapOpenApiSchemaToJsonSchema(schema: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject): JSONSchema7 | boolean {
// 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: JSONSchema7 = { ...schema } as any;
if (schema.type === 'integer') jsonSchema.type = 'number';
delete (jsonSchema as any).nullable; delete (jsonSchema as any).example; delete (jsonSchema as any).xml;
delete (jsonSchema as any).externalDocs; delete (jsonSchema as any).deprecated; delete (jsonSchema as any).readOnly; delete (jsonSchema as any).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 as JSONSchema7TypeName, 'null'];
else if (!jsonSchema.type) jsonSchema.type = 'null';
}
if (jsonSchema.type === 'object' && jsonSchema.properties) {
const mappedProps: { [key: string]: JSONSchema7 | boolean } = {};
for (const [key, propSchema] of Object.entries(jsonSchema.properties)) {
if (typeof propSchema === 'object' && propSchema !== null) mappedProps[key] = mapOpenApiSchemaToJsonSchema(propSchema as OpenAPIV3.SchemaObject);
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 as OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject);
}
return jsonSchema;
}
function generateListTools(tools: McpToolDefinition[]): string {
// 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: McpToolDefinition[], baseUrl: string | null): string {
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.';
}

View File

@ -1,166 +0,0 @@
#!/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);
});

112
src/index.ts Normal file
View File

@ -0,0 +1,112 @@
#!/usr/bin/env node
import { Command } from 'commander';
import SwaggerParser from '@apidevtools/swagger-parser';
import type { OpenAPIV3 } from 'openapi-types';
import * as fs from 'fs/promises';
import * as path from 'path';
import {
generateMcpServerCode,
generatePackageJson,
generateTsconfigJson,
generateGitignore
} from './generator.js';
interface CliOptions {
input: string;
output: string;
serverName?: string;
serverVersion?: string;
baseUrl?: string;
}
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<CliOptions>();
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) as OpenAPIV3.Document;
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();

View File

@ -1,49 +0,0 @@
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 };

View File

@ -1,766 +0,0 @@
/**
* 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 };

View File

@ -1,124 +0,0 @@
/**
* 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 };

40
src/utils.ts Normal file
View File

@ -0,0 +1,40 @@
export function titleCase(str: string): string {
// 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: string, path: string): string {
// 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;
}

15
tsconfig.json Normal file
View File

@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./build",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}