init
This commit is contained in:
commit
335643b45b
126
README.md
Normal file
126
README.md
Normal 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
30
package.json
Normal 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
323
src/config-generator.js
Normal 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
22
src/file-utils.js
Normal 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
166
src/index.js
Normal 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
49
src/openapi-loader.js
Normal 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
766
src/server-generator.js
Normal 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
124
src/tool-generator.js
Normal 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 };
|
||||
Loading…
x
Reference in New Issue
Block a user