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:
parent
2c4f2e4f49
commit
3b4f716661
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
node_modules
|
||||
.env
|
||||
build
|
||||
108
README.md
108
README.md
@ -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)
|
||||
|
||||
[](https://www.npmjs.com/package/openapi-mcp-generator)
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](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,93 +29,93 @@ 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 | |
|
||||
| :--------------- | :---- | :-------------------------------------------------------------------------------------------------- | :------------------------------------------- |
|
||||
| `--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:
|
||||
1. **Navigate to the generated directory:**
|
||||
```bash
|
||||
cd my-mcp-server
|
||||
cd <output_directory>
|
||||
# e.g., cd ./petstore-mcp
|
||||
```
|
||||
|
||||
2. Install dependencies:
|
||||
2. **Install dependencies:**
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
This installs `@modelcontextprotocol/sdk`, `axios`, `zod`, `json-schema-to-zod`, and `typescript`.
|
||||
|
||||
3. Create an environment file:
|
||||
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. **Build the server code:**
|
||||
```bash
|
||||
cp .env.example .env
|
||||
npm run build
|
||||
```
|
||||
This compiles the TypeScript code from `src/` into JavaScript in the `build/` directory.
|
||||
|
||||
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:
|
||||
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`)
|
||||
@ -124,4 +125,5 @@ Contributions are welcome! Please feel free to submit a Pull Request.
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
[MIT](https://opensource.org/licenses/MIT)
|
||||
```
|
||||
55
examples/petstore-mcp/.gitignore
vendored
Normal file
55
examples/petstore-mcp/.gitignore
vendored
Normal 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
|
||||
31
examples/petstore-mcp/package.json
Normal file
31
examples/petstore-mcp/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
2219
examples/petstore-mcp/src/index.ts
Normal file
2219
examples/petstore-mcp/src/index.ts
Normal file
File diff suppressed because it is too large
Load Diff
27
examples/petstore-mcp/tsconfig.json
Normal file
27
examples/petstore-mcp/tsconfig.json
Normal 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"
|
||||
]
|
||||
}
|
||||
38
package.json
38
package.json
@ -1,15 +1,23 @@
|
||||
{
|
||||
"name": "openapi-mcp-generator",
|
||||
"version": "1.0.4",
|
||||
"description": "Generate MCP server code from OpenAPI specifications",
|
||||
"version": "2.0.0",
|
||||
"description": "Generates MCP server code from OpenAPI specifications",
|
||||
"license": "MIT",
|
||||
"author": "Harsha",
|
||||
"type": "module",
|
||||
"main": "src/index.js",
|
||||
"bin": {
|
||||
"openapi-mcp-generator": "./src/index.js"
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"openapi-mcp-generator": "./build/index.js"
|
||||
},
|
||||
"files": [
|
||||
"build"
|
||||
],
|
||||
"scripts": {
|
||||
"start": "node src/index.js",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
"start": "node build/index.js",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"build": "tsc && chmod 755 build/index.js"
|
||||
},
|
||||
"keywords": [
|
||||
"openapi",
|
||||
@ -18,8 +26,6 @@
|
||||
"generator",
|
||||
"llm"
|
||||
],
|
||||
"author": "Harsha",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/harsha-iiiv/openapi-mcp-generator.git"
|
||||
@ -29,10 +35,16 @@
|
||||
},
|
||||
"homepage": "https://github.com/harsha-iiiv/openapi-mcp-generator#readme",
|
||||
"dependencies": {
|
||||
"axios": "^1.6.0",
|
||||
"minimist": "^1.2.8"
|
||||
"@apidevtools/swagger-parser": "^10.1.1",
|
||||
"@modelcontextprotocol/sdk": "^1.9.0",
|
||||
"axios": "^1.8.4",
|
||||
"commander": "^13.1.0",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
"devDependencies": {
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.9.0"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
};
|
||||
@ -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
604
src/generator.ts
Normal 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.';
|
||||
}
|
||||
166
src/index.js
166
src/index.js
@ -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
112
src/index.ts
Normal 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();
|
||||
@ -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 };
|
||||
@ -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 };
|
||||
@ -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
40
src/utils.ts
Normal 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
15
tsconfig.json
Normal 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"]
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user