feat: Add parser and type definitions for OpenAPI to MCP generator
- Introduced a new parser module with exports from extract-tools. - Created core type definitions for CLI options and MCP tool definitions. - Removed outdated utility functions and replaced them with new code generation utilities. - Implemented security handling utilities for API key, HTTP, and OAuth2 authentication. - Added URL handling utilities for base URL determination and query parameter management. - Updated TypeScript configuration for improved module resolution and output settings.
This commit is contained in:
parent
7e5c34bc1f
commit
6f645e06f3
18
.eslintrc.json
Normal file
18
.eslintrc.json
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"extends": [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended"
|
||||||
|
],
|
||||||
|
"plugins": ["@typescript-eslint"],
|
||||||
|
"env": {
|
||||||
|
"node": true,
|
||||||
|
"es2022": true
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"no-console": "off",
|
||||||
|
"@typescript-eslint/explicit-function-return-type": "off",
|
||||||
|
"@typescript-eslint/no-explicit-any": "off",
|
||||||
|
"@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }]
|
||||||
|
}
|
||||||
|
}
|
||||||
48
.gitignore
vendored
48
.gitignore
vendored
@ -1,2 +1,48 @@
|
|||||||
node_modules
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# Build outputs
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# Coverage
|
||||||
|
coverage/
|
||||||
|
.nyc_output/
|
||||||
|
|
||||||
|
# IDEs and editors
|
||||||
|
.idea/
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
*.code-workspace
|
||||||
|
*.sublime-workspace
|
||||||
|
*.sublime-project
|
||||||
|
|
||||||
|
# OS specific
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
.env
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.npm
|
||||||
|
.eslintcache
|
||||||
7
.prettierrc
Normal file
7
.prettierrc
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"semi": true,
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"singleQuote": true,
|
||||||
|
"printWidth": 100,
|
||||||
|
"tabWidth": 2
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,82 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
import { Command } from 'commander';
|
|
||||||
import SwaggerParser from '@apidevtools/swagger-parser';
|
|
||||||
import * as fs from 'fs/promises';
|
|
||||||
import * as path from 'path';
|
|
||||||
import { generateMcpServerCode, generatePackageJson, generateTsconfigJson, generateGitignore } from './generator.js';
|
|
||||||
const program = new Command();
|
|
||||||
program
|
|
||||||
.name('openapi-mcp-generator')
|
|
||||||
.description('Generates a buildable MCP server project (TypeScript) from an OpenAPI specification')
|
|
||||||
// Ensure these option definitions are robust
|
|
||||||
.requiredOption('-i, --input <file_or_url>', 'Path or URL to the OpenAPI specification file (JSON or YAML)')
|
|
||||||
.requiredOption('-o, --output <directory>', 'Path to the directory where the MCP server project will be created (e.g., ./petstore-mcp)')
|
|
||||||
.option('-n, --server-name <name>', 'Name for the generated MCP server package (default: derived from OpenAPI info title)')
|
|
||||||
.option('-v, --server-version <version>', 'Version for the generated MCP server (default: derived from OpenAPI info version or 0.1.0)')
|
|
||||||
.option('-b, --base-url <url>', 'Base URL for the target API. Required if not specified in OpenAPI `servers` or if multiple servers exist.')
|
|
||||||
.version('2.0.0'); // Match package.json version
|
|
||||||
// Parse arguments explicitly from process.argv
|
|
||||||
// This is generally the most reliable way
|
|
||||||
program.parse(process.argv);
|
|
||||||
// Retrieve the options AFTER parsing
|
|
||||||
const options = program.opts();
|
|
||||||
async function main() {
|
|
||||||
// Use the parsed options directly
|
|
||||||
const outputDir = options.output;
|
|
||||||
const inputSpec = options.input; // Use the parsed input value
|
|
||||||
const srcDir = path.join(outputDir, 'src');
|
|
||||||
const serverFilePath = path.join(srcDir, 'index.ts');
|
|
||||||
const packageJsonPath = path.join(outputDir, 'package.json');
|
|
||||||
const tsconfigPath = path.join(outputDir, 'tsconfig.json');
|
|
||||||
const gitignorePath = path.join(outputDir, '.gitignore');
|
|
||||||
try {
|
|
||||||
// Use the correct inputSpec variable
|
|
||||||
console.error(`Parsing OpenAPI spec: ${inputSpec}`);
|
|
||||||
const api = await SwaggerParser.dereference(inputSpec);
|
|
||||||
console.error('OpenAPI spec parsed successfully.');
|
|
||||||
// Use options directly for name/version/baseUrl determination
|
|
||||||
const serverNameRaw = options.serverName || api.info.title || 'my-mcp-server';
|
|
||||||
const serverName = serverNameRaw.toLowerCase().replace(/[^a-z0-9_-]/g, '-');
|
|
||||||
const serverVersion = options.serverVersion || api.info.version || '0.1.0';
|
|
||||||
console.error('Generating server code...');
|
|
||||||
// Pass inputSpec to generator function if needed for comments, otherwise just options
|
|
||||||
const serverTsContent = generateMcpServerCode(api, options, serverName, serverVersion);
|
|
||||||
console.error('Generating package.json...');
|
|
||||||
const packageJsonContent = generatePackageJson(serverName, serverVersion);
|
|
||||||
console.error('Generating tsconfig.json...');
|
|
||||||
const tsconfigJsonContent = generateTsconfigJson();
|
|
||||||
console.error('Generating .gitignore...');
|
|
||||||
const gitignoreContent = generateGitignore();
|
|
||||||
console.error(`Creating project directory structure at: ${outputDir}`);
|
|
||||||
await fs.mkdir(srcDir, { recursive: true });
|
|
||||||
await fs.writeFile(serverFilePath, serverTsContent);
|
|
||||||
console.error(` -> Created ${serverFilePath}`);
|
|
||||||
await fs.writeFile(packageJsonPath, packageJsonContent);
|
|
||||||
console.error(` -> Created ${packageJsonPath}`);
|
|
||||||
await fs.writeFile(tsconfigPath, tsconfigJsonContent);
|
|
||||||
console.error(` -> Created ${tsconfigPath}`);
|
|
||||||
await fs.writeFile(gitignorePath, gitignoreContent);
|
|
||||||
console.error(` -> Created ${gitignorePath}`);
|
|
||||||
console.error("\n---");
|
|
||||||
console.error(`MCP server project '${serverName}' successfully generated at: ${outputDir}`);
|
|
||||||
console.error("\nNext steps:");
|
|
||||||
console.error(`1. Navigate to the directory: cd ${outputDir}`);
|
|
||||||
console.error(`2. Install dependencies: npm install`);
|
|
||||||
console.error(`3. Build the TypeScript code: npm run build`);
|
|
||||||
console.error(`4. Run the server: npm start`);
|
|
||||||
console.error(" (This runs the built JavaScript code in build/index.js)");
|
|
||||||
console.error("---");
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
console.error('\nError generating MCP server project:', error);
|
|
||||||
try {
|
|
||||||
await fs.rm(outputDir, { recursive: true, force: true });
|
|
||||||
console.error(`Cleaned up partially created directory: ${outputDir}`);
|
|
||||||
}
|
|
||||||
catch (cleanupError) {
|
|
||||||
console.error(`Failed to cleanup directory ${outputDir}:`, cleanupError);
|
|
||||||
}
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
main();
|
|
||||||
@ -1,35 +0,0 @@
|
|||||||
export function titleCase(str) {
|
|
||||||
// Converts snake_case, kebab-case, or path/parts to TitleCase
|
|
||||||
return str
|
|
||||||
.toLowerCase()
|
|
||||||
.replace(/[-_\/](.)/g, (_, char) => char.toUpperCase()) // Handle separators
|
|
||||||
.replace(/^{/, '') // Remove leading { from path params
|
|
||||||
.replace(/}$/, '') // Remove trailing } from path params
|
|
||||||
.replace(/^./, (char) => char.toUpperCase()); // Capitalize first letter
|
|
||||||
}
|
|
||||||
export function generateOperationId(method, path) {
|
|
||||||
// Generator: get /users/{userId}/posts -> GetUsersPostsByUserId
|
|
||||||
const parts = path.split('/').filter(p => p); // Split and remove empty parts
|
|
||||||
let name = method.toLowerCase(); // Start with method name
|
|
||||||
parts.forEach((part, index) => {
|
|
||||||
if (part.startsWith('{') && part.endsWith('}')) {
|
|
||||||
// Append 'By' + ParamName only for the *last* path parameter segment
|
|
||||||
if (index === parts.length - 1) {
|
|
||||||
name += 'By' + titleCase(part);
|
|
||||||
}
|
|
||||||
// Potentially include non-terminal params differently if needed, e.g.:
|
|
||||||
// else { name += 'With' + titleCase(part); }
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Append the static path part in TitleCase
|
|
||||||
name += titleCase(part);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// Simple fallback if name is just the method (e.g., GET /)
|
|
||||||
if (name === method.toLowerCase()) {
|
|
||||||
name += 'Root';
|
|
||||||
}
|
|
||||||
// Ensure first letter is uppercase after potential lowercase method start
|
|
||||||
name = name.charAt(0).toUpperCase() + name.slice(1);
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
97
package.json
97
package.json
@ -1,50 +1,65 @@
|
|||||||
{
|
{
|
||||||
"name": "openapi-mcp-generator",
|
"name": "openapi-mcp-generator",
|
||||||
"version": "2.0.0",
|
"version": "2.5.0-beta.0",
|
||||||
"description": "Generates MCP server code from OpenAPI specifications",
|
"description": "Generates MCP server code from OpenAPI specifications",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"author": "Harsha",
|
"author": "Harsha",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0"
|
"node": ">=20.0.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"openapi-mcp-generator": "./build/index.js"
|
"openapi-mcp-generator": "./dist/index.js"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"build"
|
"dist",
|
||||||
],
|
"README.md",
|
||||||
"scripts": {
|
"LICENSE"
|
||||||
"start": "node build/index.js",
|
],
|
||||||
"typecheck": "tsc --noEmit",
|
"scripts": {
|
||||||
"build": "tsc && chmod 755 build/index.js"
|
"start": "node dist/index.js",
|
||||||
},
|
"clean": "rimraf dist",
|
||||||
"keywords": [
|
"typecheck": "tsc --noEmit",
|
||||||
"openapi",
|
"build": "tsc && chmod 755 dist/index.js",
|
||||||
"mcp",
|
"prepare": "npm run clean && npm run build",
|
||||||
"model-context-protocol",
|
"prepublishOnly": "npm run lint",
|
||||||
"generator",
|
"lint": "eslint src --ext .ts",
|
||||||
"llm"
|
"format": "prettier --write \"src/**/*.ts\""
|
||||||
],
|
},
|
||||||
"repository": {
|
"keywords": [
|
||||||
"type": "git",
|
"openapi",
|
||||||
"url": "git+https://github.com/harsha-iiiv/openapi-mcp-generator.git"
|
"mcp",
|
||||||
},
|
"model-context-protocol",
|
||||||
"bugs": {
|
"generator",
|
||||||
"url": "https://github.com/harsha-iiiv/openapi-mcp-generator/issues"
|
"llm",
|
||||||
},
|
"ai",
|
||||||
"homepage": "https://github.com/harsha-iiiv/openapi-mcp-generator#readme",
|
"api"
|
||||||
"dependencies": {
|
],
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/harsha-iiiv/openapi-mcp-generator.git"
|
||||||
|
},
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/harsha-iiiv/openapi-mcp-generator/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/harsha-iiiv/openapi-mcp-generator#readme",
|
||||||
|
"dependencies": {
|
||||||
"@apidevtools/swagger-parser": "^10.1.1",
|
"@apidevtools/swagger-parser": "^10.1.1",
|
||||||
"@modelcontextprotocol/sdk": "^1.9.0",
|
|
||||||
"axios": "^1.8.4",
|
|
||||||
"commander": "^13.1.0",
|
"commander": "^13.1.0",
|
||||||
"zod": "^3.24.2"
|
"openapi-types": "^12.1.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.14.1",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^8.29.1",
|
||||||
|
"@typescript-eslint/parser": "^8.29.1",
|
||||||
|
"eslint": "^9.24.0",
|
||||||
|
"prettier": "^3.5.3",
|
||||||
|
"rimraf": "^6.0.1",
|
||||||
"typescript": "^5.8.3"
|
"typescript": "^5.8.3"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@modelcontextprotocol/sdk": "^1.9.0"
|
"@modelcontextprotocol/sdk": "^1.9.0",
|
||||||
|
"zod": "^3.24.2",
|
||||||
|
"json-schema-to-zod": "^2.4.1"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
604
src/generator.ts
604
src/generator.ts
File diff suppressed because it is too large
Load Diff
178
src/generator/config-files.ts
Normal file
178
src/generator/config-files.ts
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
/**
|
||||||
|
* Generator for configuration files for MCP servers
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates the content of tsconfig.json for the MCP server
|
||||||
|
*
|
||||||
|
* @returns JSON string for tsconfig.json
|
||||||
|
*/
|
||||||
|
export function generateTsconfigJson(): string {
|
||||||
|
const tsconfigData = {
|
||||||
|
compilerOptions: {
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"target": "ES2022",
|
||||||
|
"allowJs": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"strict": true,
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"module": "Node16",
|
||||||
|
"moduleResolution": "Node16",
|
||||||
|
"noEmit": false,
|
||||||
|
"outDir": "./build",
|
||||||
|
"declaration": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"forceConsistentCasingInFileNames": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "build", "**/*.test.ts"]
|
||||||
|
};
|
||||||
|
|
||||||
|
return JSON.stringify(tsconfigData, null, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates the content of .gitignore for the MCP server
|
||||||
|
*
|
||||||
|
* @returns Content for .gitignore
|
||||||
|
*/
|
||||||
|
export function generateGitignore(): string {
|
||||||
|
return `# Dependencies
|
||||||
|
node_modules
|
||||||
|
.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# Build outputs
|
||||||
|
dist
|
||||||
|
build
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# Reports
|
||||||
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Coverage
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Build artifacts
|
||||||
|
.grunt
|
||||||
|
bower_components
|
||||||
|
jspm_packages/
|
||||||
|
web_modules/
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Editor settings
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
*.code-workspace
|
||||||
|
.idea
|
||||||
|
*.sublime-workspace
|
||||||
|
*.sublime-project
|
||||||
|
|
||||||
|
# Caches
|
||||||
|
.eslintcache
|
||||||
|
.stylelintcache
|
||||||
|
.node_repl_history
|
||||||
|
.browserslistcache
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
# OS specific
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates the content of .eslintrc.json for the MCP server
|
||||||
|
*
|
||||||
|
* @returns JSON string for .eslintrc.json
|
||||||
|
*/
|
||||||
|
export function generateEslintConfig(): string {
|
||||||
|
const eslintConfig = {
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"extends": [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended"
|
||||||
|
],
|
||||||
|
"plugins": ["@typescript-eslint"],
|
||||||
|
"env": {
|
||||||
|
"node": true,
|
||||||
|
"es2022": true
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"no-console": ["error", { "allow": ["error", "warn"] }],
|
||||||
|
"@typescript-eslint/explicit-function-return-type": "off",
|
||||||
|
"@typescript-eslint/no-explicit-any": "off",
|
||||||
|
"@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return JSON.stringify(eslintConfig, null, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates the content of jest.config.js for the MCP server
|
||||||
|
*
|
||||||
|
* @returns Content for jest.config.js
|
||||||
|
*/
|
||||||
|
export function generateJestConfig(): string {
|
||||||
|
return `export default {
|
||||||
|
preset: 'ts-jest',
|
||||||
|
testEnvironment: 'node',
|
||||||
|
extensionsToTreatAsEsm: ['.ts'],
|
||||||
|
moduleNameMapper: {
|
||||||
|
'^(\\.{1,2}/.*)\\.js$': '$1',
|
||||||
|
},
|
||||||
|
transform: {
|
||||||
|
'^.+\\.tsx?$': [
|
||||||
|
'ts-jest',
|
||||||
|
{
|
||||||
|
useESM: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates the content of .prettierrc for the MCP server
|
||||||
|
*
|
||||||
|
* @returns JSON string for .prettierrc
|
||||||
|
*/
|
||||||
|
export function generatePrettierConfig(): string {
|
||||||
|
const prettierConfig = {
|
||||||
|
"semi": true,
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"singleQuote": true,
|
||||||
|
"printWidth": 100,
|
||||||
|
"tabWidth": 2
|
||||||
|
};
|
||||||
|
|
||||||
|
return JSON.stringify(prettierConfig, null, 2);
|
||||||
|
}
|
||||||
96
src/generator/env-file.ts
Normal file
96
src/generator/env-file.ts
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
/**
|
||||||
|
* Generator for .env file and .env.example file
|
||||||
|
*/
|
||||||
|
import { OpenAPIV3 } from 'openapi-types';
|
||||||
|
import { getEnvVarName } from '../utils/security.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates the content of .env.example file for the MCP server
|
||||||
|
*
|
||||||
|
* @param securitySchemes Security schemes from the OpenAPI spec
|
||||||
|
* @returns Content for .env.example file
|
||||||
|
*/
|
||||||
|
export function generateEnvExample(securitySchemes?: OpenAPIV3.ComponentsObject['securitySchemes']): string {
|
||||||
|
let content = `# MCP Server Environment Variables
|
||||||
|
# Copy this file to .env and fill in the values
|
||||||
|
|
||||||
|
# Server configuration
|
||||||
|
PORT=3000
|
||||||
|
LOG_LEVEL=info
|
||||||
|
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Add security scheme environment variables with examples
|
||||||
|
if (securitySchemes && Object.keys(securitySchemes).length > 0) {
|
||||||
|
content += `# API Authentication\n`;
|
||||||
|
|
||||||
|
for (const [name, schemeOrRef] of Object.entries(securitySchemes)) {
|
||||||
|
if ('$ref' in schemeOrRef) {
|
||||||
|
content += `# ${name} - Referenced security scheme (reference not resolved)\n`;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const scheme = schemeOrRef;
|
||||||
|
|
||||||
|
if (scheme.type === 'apiKey') {
|
||||||
|
const varName = getEnvVarName(name, 'API_KEY');
|
||||||
|
content += `${varName}=your_api_key_here\n`;
|
||||||
|
}
|
||||||
|
else if (scheme.type === 'http') {
|
||||||
|
if (scheme.scheme?.toLowerCase() === 'bearer') {
|
||||||
|
const varName = getEnvVarName(name, 'BEARER_TOKEN');
|
||||||
|
content += `${varName}=your_bearer_token_here\n`;
|
||||||
|
}
|
||||||
|
else if (scheme.scheme?.toLowerCase() === 'basic') {
|
||||||
|
const usernameVar = getEnvVarName(name, 'BASIC_USERNAME');
|
||||||
|
const passwordVar = getEnvVarName(name, 'BASIC_PASSWORD');
|
||||||
|
content += `${usernameVar}=your_username_here\n`;
|
||||||
|
content += `${passwordVar}=your_password_here\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (scheme.type === 'oauth2') {
|
||||||
|
content += `# OAuth2 authentication (${scheme.flows ? Object.keys(scheme.flows).join(', ') : 'unknown'} flow)\n`;
|
||||||
|
const varName = `OAUTH_TOKEN_${name.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}`;
|
||||||
|
content += `${varName}=your_oauth_token_here\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
content += `# No API authentication required\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
content += `\n# Add any other environment variables your API might need\n`;
|
||||||
|
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates dotenv configuration code for the MCP server
|
||||||
|
*
|
||||||
|
* @returns Code for loading environment variables
|
||||||
|
*/
|
||||||
|
export function generateDotenvConfig(): string {
|
||||||
|
return `
|
||||||
|
/**
|
||||||
|
* Load environment variables from .env file
|
||||||
|
*/
|
||||||
|
import dotenv from 'dotenv';
|
||||||
|
import path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
|
// Load environment variables from .env file
|
||||||
|
const result = dotenv.config({ path: path.resolve(__dirname, '../.env') });
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
console.warn('Warning: No .env file found or error loading .env file.');
|
||||||
|
console.warn('Using default environment variables.');
|
||||||
|
}
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
port: process.env.PORT || '3000',
|
||||||
|
logLevel: process.env.LOG_LEVEL || 'info',
|
||||||
|
};
|
||||||
|
`;
|
||||||
|
}
|
||||||
9
src/generator/index.ts
Normal file
9
src/generator/index.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
/**
|
||||||
|
* Generator module exports
|
||||||
|
*/
|
||||||
|
export * from './server-code.js';
|
||||||
|
export * from './package-json.js';
|
||||||
|
export * from './config-files.js';
|
||||||
|
export * from './env-file.js';
|
||||||
|
export * from './oauth-docs.js';
|
||||||
|
export * from './web-server.js';
|
||||||
109
src/generator/oauth-docs.ts
Normal file
109
src/generator/oauth-docs.ts
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
/**
|
||||||
|
* Generator for OAuth2 documentation
|
||||||
|
*/
|
||||||
|
import { OpenAPIV3 } from 'openapi-types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates documentation about OAuth2 configuration
|
||||||
|
*
|
||||||
|
* @param securitySchemes Security schemes from OpenAPI spec
|
||||||
|
* @returns Markdown documentation about OAuth2 configuration
|
||||||
|
*/
|
||||||
|
export function generateOAuth2Docs(securitySchemes?: OpenAPIV3.ComponentsObject['securitySchemes']): string {
|
||||||
|
if (!securitySchemes) {
|
||||||
|
return "# OAuth2 Configuration\n\nNo OAuth2 security schemes defined in this API.";
|
||||||
|
}
|
||||||
|
|
||||||
|
let oauth2Schemes: {name: string, scheme: OpenAPIV3.OAuth2SecurityScheme}[] = [];
|
||||||
|
|
||||||
|
// Find OAuth2 schemes
|
||||||
|
for (const [name, schemeOrRef] of Object.entries(securitySchemes)) {
|
||||||
|
if ('$ref' in schemeOrRef) continue;
|
||||||
|
|
||||||
|
if (schemeOrRef.type === 'oauth2') {
|
||||||
|
oauth2Schemes.push({
|
||||||
|
name,
|
||||||
|
scheme: schemeOrRef
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oauth2Schemes.length === 0) {
|
||||||
|
return "# OAuth2 Configuration\n\nNo OAuth2 security schemes defined in this API.";
|
||||||
|
}
|
||||||
|
|
||||||
|
let content = `# OAuth2 Configuration
|
||||||
|
|
||||||
|
This API uses OAuth2 for authentication. The MCP server can handle OAuth2 authentication in the following ways:
|
||||||
|
|
||||||
|
1. **Using a pre-acquired token**: You provide a token you've already obtained
|
||||||
|
2. **Using client credentials flow**: The server automatically acquires a token using your client ID and secret
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Document each OAuth2 scheme
|
||||||
|
for (const {name, scheme} of oauth2Schemes) {
|
||||||
|
content += `### ${name}\n\n`;
|
||||||
|
|
||||||
|
if (scheme.description) {
|
||||||
|
content += `${scheme.description}\n\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
content += "**Configuration Variables:**\n\n";
|
||||||
|
|
||||||
|
const envVarPrefix = name.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase();
|
||||||
|
|
||||||
|
content += `- \`OAUTH_CLIENT_ID_${envVarPrefix}\`: Your OAuth client ID\n`;
|
||||||
|
content += `- \`OAUTH_CLIENT_SECRET_${envVarPrefix}\`: Your OAuth client secret\n`;
|
||||||
|
|
||||||
|
if (scheme.flows?.clientCredentials) {
|
||||||
|
content += `- \`OAUTH_SCOPES_${envVarPrefix}\`: Space-separated list of scopes to request (optional)\n`;
|
||||||
|
content += `- \`OAUTH_TOKEN_${envVarPrefix}\`: Pre-acquired OAuth token (optional if using client credentials)\n\n`;
|
||||||
|
|
||||||
|
content += "**Client Credentials Flow:**\n\n";
|
||||||
|
content += `- Token URL: \`${scheme.flows.clientCredentials.tokenUrl}\`\n`;
|
||||||
|
|
||||||
|
if (scheme.flows.clientCredentials.scopes && Object.keys(scheme.flows.clientCredentials.scopes).length > 0) {
|
||||||
|
content += "\n**Available Scopes:**\n\n";
|
||||||
|
|
||||||
|
for (const [scope, description] of Object.entries(scheme.flows.clientCredentials.scopes)) {
|
||||||
|
content += `- \`${scope}\`: ${description}\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
content += "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scheme.flows?.authorizationCode) {
|
||||||
|
content += `- \`OAUTH_TOKEN_${envVarPrefix}\`: Pre-acquired OAuth token (required for authorization code flow)\n\n`;
|
||||||
|
|
||||||
|
content += "**Authorization Code Flow:**\n\n";
|
||||||
|
content += `- Authorization URL: \`${scheme.flows.authorizationCode.authorizationUrl}\`\n`;
|
||||||
|
content += `- Token URL: \`${scheme.flows.authorizationCode.tokenUrl}\`\n`;
|
||||||
|
|
||||||
|
if (scheme.flows.authorizationCode.scopes && Object.keys(scheme.flows.authorizationCode.scopes).length > 0) {
|
||||||
|
content += "\n**Available Scopes:**\n\n";
|
||||||
|
|
||||||
|
for (const [scope, description] of Object.entries(scheme.flows.authorizationCode.scopes)) {
|
||||||
|
content += `- \`${scope}\`: ${description}\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
content += "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
content += `## Token Caching
|
||||||
|
|
||||||
|
The MCP server automatically caches OAuth tokens obtained via client credentials flow. Tokens are cached for their lifetime (as specified by the \`expires_in\` parameter in the token response) minus 60 seconds as a safety margin.
|
||||||
|
|
||||||
|
When making API requests, the server will:
|
||||||
|
1. Check for a cached token that's still valid
|
||||||
|
2. Use the cached token if available
|
||||||
|
3. Request a new token if no valid cached token exists
|
||||||
|
`;
|
||||||
|
|
||||||
|
return content;
|
||||||
|
}
|
||||||
65
src/generator/package-json.ts
Normal file
65
src/generator/package-json.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
/**
|
||||||
|
* Generates the content of package.json for the MCP server
|
||||||
|
*
|
||||||
|
* @param serverName Server name
|
||||||
|
* @param serverVersion Server version
|
||||||
|
* @param includeWebDeps Whether to include web server dependencies
|
||||||
|
* @returns JSON string for package.json
|
||||||
|
*/
|
||||||
|
export function generatePackageJson(
|
||||||
|
serverName: string,
|
||||||
|
serverVersion: string,
|
||||||
|
includeWebDeps: boolean = false
|
||||||
|
): string {
|
||||||
|
const packageData: any = {
|
||||||
|
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": ">=20.0.0"
|
||||||
|
},
|
||||||
|
dependencies: {
|
||||||
|
"@modelcontextprotocol/sdk": "^1.9.0",
|
||||||
|
"axios": "^1.8.4",
|
||||||
|
"dotenv": "^16.4.5",
|
||||||
|
"zod": "^3.24.2",
|
||||||
|
"json-schema-to-zod": "^2.4.1"
|
||||||
|
},
|
||||||
|
devDependencies: {
|
||||||
|
"@types/node": "^18.19.0",
|
||||||
|
"typescript": "^5.8.3"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add web server dependencies if needed
|
||||||
|
if (includeWebDeps) {
|
||||||
|
packageData.dependencies = {
|
||||||
|
...packageData.dependencies,
|
||||||
|
"express": "^4.18.2",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"uuid": "^11.1.0"
|
||||||
|
};
|
||||||
|
|
||||||
|
packageData.devDependencies = {
|
||||||
|
...packageData.devDependencies,
|
||||||
|
"@types/express": "^4.17.21",
|
||||||
|
"@types/cors": "^2.8.17",
|
||||||
|
"@types/uuid": "^10.0.0"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add a script to start in web mode
|
||||||
|
packageData.scripts["start:web"] = "node build/index.js --transport=web";
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify(packageData, null, 2);
|
||||||
|
}
|
||||||
227
src/generator/server-code.ts
Normal file
227
src/generator/server-code.ts
Normal file
@ -0,0 +1,227 @@
|
|||||||
|
import { OpenAPIV3 } from 'openapi-types';
|
||||||
|
import { CliOptions } from '../types/index.js';
|
||||||
|
import { extractToolsFromApi } from '../parser/extract-tools.js';
|
||||||
|
import { determineBaseUrl } from '../utils/index.js';
|
||||||
|
import {
|
||||||
|
generateToolDefinitionMap,
|
||||||
|
generateCallToolHandler,
|
||||||
|
generateListToolsHandler
|
||||||
|
} from '../utils/code-gen.js';
|
||||||
|
import { generateExecuteApiToolFunction } from '../utils/security.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates the TypeScript code for the MCP server
|
||||||
|
*
|
||||||
|
* @param api OpenAPI document
|
||||||
|
* @param options CLI options
|
||||||
|
* @param serverName Server name
|
||||||
|
* @param serverVersion Server version
|
||||||
|
* @returns Generated TypeScript code
|
||||||
|
*/
|
||||||
|
export function generateMcpServerCode(
|
||||||
|
api: OpenAPIV3.Document,
|
||||||
|
options: CliOptions,
|
||||||
|
serverName: string,
|
||||||
|
serverVersion: string
|
||||||
|
): string {
|
||||||
|
// Extract tools from API
|
||||||
|
const tools = extractToolsFromApi(api);
|
||||||
|
|
||||||
|
// Determine base URL
|
||||||
|
const determinedBaseUrl = determineBaseUrl(api, options.baseUrl);
|
||||||
|
|
||||||
|
// Generate code for tool definition map
|
||||||
|
const toolDefinitionMapCode = generateToolDefinitionMap(tools, api.components?.securitySchemes);
|
||||||
|
|
||||||
|
// Generate code for API tool execution
|
||||||
|
const executeApiToolFunctionCode = generateExecuteApiToolFunction(api.components?.securitySchemes);
|
||||||
|
|
||||||
|
// Generate code for request handlers
|
||||||
|
const callToolHandlerCode = generateCallToolHandler();
|
||||||
|
const listToolsHandlerCode = generateListToolsHandler();
|
||||||
|
|
||||||
|
// Determine if we should include web server code
|
||||||
|
const includeWebServer = options.transport === 'web';
|
||||||
|
const webServerImport = includeWebServer
|
||||||
|
? `\nimport { setupWebServer } from "./web-server.js";`
|
||||||
|
: '';
|
||||||
|
|
||||||
|
// Define transport based on options
|
||||||
|
const transportCode = includeWebServer
|
||||||
|
? `// Set up Web Server transport
|
||||||
|
try {
|
||||||
|
await setupWebServer(server, ${options.port || 3000});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error setting up web server:", error);
|
||||||
|
process.exit(1);
|
||||||
|
}`
|
||||||
|
: `// Set up stdio transport
|
||||||
|
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);
|
||||||
|
}`;
|
||||||
|
|
||||||
|
// Generate the full server code
|
||||||
|
return `#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* MCP Server generated from OpenAPI spec for ${serverName} v${serverVersion}
|
||||||
|
* Generated on: ${new Date().toISOString()}
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Load environment variables from .env file
|
||||||
|
import dotenv from 'dotenv';
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||||
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||||
|
import {
|
||||||
|
CallToolRequestSchema,
|
||||||
|
ListToolsRequestSchema,
|
||||||
|
type Tool,
|
||||||
|
type CallToolResult,
|
||||||
|
type CallToolRequest
|
||||||
|
} from "@modelcontextprotocol/sdk/types.js";${webServerImport}
|
||||||
|
|
||||||
|
import { z, ZodError } from 'zod';
|
||||||
|
import { jsonSchemaToZod } from 'json-schema-to-zod';
|
||||||
|
import axios, { type AxiosRequestConfig, type AxiosError } from 'axios';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type definition for JSON objects
|
||||||
|
*/
|
||||||
|
type JsonObject = Record<string, any>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for MCP Tool Definition
|
||||||
|
*/
|
||||||
|
interface McpToolDefinition {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
inputSchema: any;
|
||||||
|
method: string;
|
||||||
|
pathTemplate: string;
|
||||||
|
executionParameters: { name: string, in: string }[];
|
||||||
|
requestBodyContentType?: string;
|
||||||
|
securityRequirements: any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Server configuration
|
||||||
|
*/
|
||||||
|
export const SERVER_NAME = "${serverName}";
|
||||||
|
export const SERVER_VERSION = "${serverVersion}";
|
||||||
|
export const API_BASE_URL = "${determinedBaseUrl || ''}";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MCP Server instance
|
||||||
|
*/
|
||||||
|
const server = new Server(
|
||||||
|
{ name: SERVER_NAME, version: SERVER_VERSION },
|
||||||
|
{ capabilities: { tools: {} } }
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map of tool definitions by name
|
||||||
|
*/
|
||||||
|
const toolDefinitionMap: Map<string, McpToolDefinition> = new Map([
|
||||||
|
${toolDefinitionMapCode}
|
||||||
|
]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Security schemes from the OpenAPI spec
|
||||||
|
*/
|
||||||
|
const securitySchemes = ${JSON.stringify(api.components?.securitySchemes || {}, null, 2).replace(/^/gm, ' ')};
|
||||||
|
|
||||||
|
${listToolsHandlerCode}
|
||||||
|
${callToolHandlerCode}
|
||||||
|
${executeApiToolFunctionCode}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main function to start the server
|
||||||
|
*/
|
||||||
|
async function main() {
|
||||||
|
${transportCode}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleanup function for graceful shutdown
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats API errors for better readability
|
||||||
|
*
|
||||||
|
* @param error Axios error
|
||||||
|
* @returns Formatted error message
|
||||||
|
*/
|
||||||
|
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 data]';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
message += 'No response body received.';
|
||||||
|
}
|
||||||
|
} else if (error.request) {
|
||||||
|
message = 'API Network Error: No response received from server.';
|
||||||
|
if (error.code) message += \` (Code: \${error.code})\`;
|
||||||
|
} else {
|
||||||
|
message += \`API Request Setup Error: \${error.message}\`;
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a JSON Schema to a Zod schema for runtime validation
|
||||||
|
*
|
||||||
|
* @param jsonSchema JSON Schema
|
||||||
|
* @param toolName Tool name for error reporting
|
||||||
|
* @returns Zod schema
|
||||||
|
*/
|
||||||
|
function getZodSchemaFromJsonSchema(jsonSchema: any, toolName: string): z.ZodTypeAny {
|
||||||
|
if (typeof jsonSchema !== 'object' || jsonSchema === null) {
|
||||||
|
return z.object({}).passthrough();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const zodSchemaString = jsonSchemaToZod(jsonSchema);
|
||||||
|
const zodSchema = eval(zodSchemaString);
|
||||||
|
if (typeof zodSchema?.parse !== 'function') {
|
||||||
|
throw new Error('Eval did not produce a valid Zod schema.');
|
||||||
|
}
|
||||||
|
return zodSchema as z.ZodTypeAny;
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error(\`Failed to generate/evaluate Zod schema for '\${toolName}':\`, err);
|
||||||
|
return z.object({}).passthrough();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
507
src/generator/web-server.ts
Normal file
507
src/generator/web-server.ts
Normal file
File diff suppressed because it is too large
Load Diff
286
src/index.ts
286
src/index.ts
@ -1,112 +1,226 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* OpenAPI to MCP Generator
|
||||||
|
*
|
||||||
|
* This tool generates a Model Context Protocol (MCP) server from an OpenAPI specification.
|
||||||
|
* It creates a Node.js project that implements MCP over stdio to proxy API requests.
|
||||||
|
*/
|
||||||
|
import fs from 'fs/promises';
|
||||||
|
import path from 'path';
|
||||||
import { Command } from 'commander';
|
import { Command } from 'commander';
|
||||||
import SwaggerParser from '@apidevtools/swagger-parser';
|
import SwaggerParser from '@apidevtools/swagger-parser';
|
||||||
import type { OpenAPIV3 } from 'openapi-types';
|
import { OpenAPIV3 } from 'openapi-types';
|
||||||
import * as fs from 'fs/promises';
|
|
||||||
import * as path from 'path';
|
// Import generators
|
||||||
import {
|
import {
|
||||||
generateMcpServerCode,
|
generateMcpServerCode,
|
||||||
generatePackageJson,
|
generatePackageJson,
|
||||||
generateTsconfigJson,
|
generateTsconfigJson,
|
||||||
generateGitignore
|
generateGitignore,
|
||||||
} from './generator.js';
|
generateEslintConfig,
|
||||||
|
generateJestConfig,
|
||||||
|
generatePrettierConfig,
|
||||||
|
generateEnvExample,
|
||||||
|
generateOAuth2Docs,
|
||||||
|
generateWebServerCode,
|
||||||
|
generateTestClientHtml
|
||||||
|
} from './generator/index.js';
|
||||||
|
|
||||||
interface CliOptions {
|
// Import types
|
||||||
input: string;
|
import { CliOptions } from './types/index.js';
|
||||||
output: string;
|
|
||||||
serverName?: string;
|
|
||||||
serverVersion?: string;
|
|
||||||
baseUrl?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Configure CLI
|
||||||
const program = new Command();
|
const program = new Command();
|
||||||
|
|
||||||
program
|
program
|
||||||
.name('openapi-mcp-generator')
|
.name('openapi-mcp-generator')
|
||||||
.description('Generates a buildable MCP server project (TypeScript) from an OpenAPI specification')
|
.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('-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)')
|
||||||
.requiredOption('-o, --output <directory>', 'Path to the directory where the MCP server project will be created (e.g., ./petstore-mcp)')
|
.option('-n, --server-name <n>', 'Name for the generated MCP server package (default: derived from OpenAPI info title)')
|
||||||
.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('-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.')
|
||||||
.option('-b, --base-url <url>', 'Base URL for the target API. Required if not specified in OpenAPI `servers` or if multiple servers exist.')
|
.option('-t, --transport <type>', 'Server transport type: "stdio" or "web" (default: "stdio")')
|
||||||
.version('2.0.0'); // Match package.json version
|
.option('-p, --port <number>', 'Port for web server (used with --transport=web, default: 3000)', (val) => parseInt(val, 10))
|
||||||
|
.option('--force', 'Overwrite existing files without prompting')
|
||||||
|
.version('2.0.0'); // Match package.json version
|
||||||
|
|
||||||
// Parse arguments explicitly from process.argv
|
// Parse arguments explicitly from process.argv
|
||||||
// This is generally the most reliable way
|
|
||||||
program.parse(process.argv);
|
program.parse(process.argv);
|
||||||
|
|
||||||
// Retrieve the options AFTER parsing
|
// Retrieve the options AFTER parsing
|
||||||
const options = program.opts<CliOptions>();
|
const options = program.opts<CliOptions & { force?: boolean }>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main function to run the generator
|
||||||
|
*/
|
||||||
async function main() {
|
async function main() {
|
||||||
// Use the parsed options directly
|
// Use the parsed options directly
|
||||||
const outputDir = options.output;
|
const outputDir = options.output;
|
||||||
const inputSpec = options.input; // Use the parsed input value
|
const inputSpec = options.input;
|
||||||
|
|
||||||
const srcDir = path.join(outputDir, 'src');
|
const srcDir = path.join(outputDir, 'src');
|
||||||
const serverFilePath = path.join(srcDir, 'index.ts');
|
const serverFilePath = path.join(srcDir, 'index.ts');
|
||||||
const packageJsonPath = path.join(outputDir, 'package.json');
|
const packageJsonPath = path.join(outputDir, 'package.json');
|
||||||
const tsconfigPath = path.join(outputDir, 'tsconfig.json');
|
const tsconfigPath = path.join(outputDir, 'tsconfig.json');
|
||||||
const gitignorePath = path.join(outputDir, '.gitignore');
|
const gitignorePath = path.join(outputDir, '.gitignore');
|
||||||
|
const eslintPath = path.join(outputDir, '.eslintrc.json');
|
||||||
|
const prettierPath = path.join(outputDir, '.prettierrc');
|
||||||
|
const jestConfigPath = path.join(outputDir, 'jest.config.js');
|
||||||
|
const envExamplePath = path.join(outputDir, '.env.example');
|
||||||
|
const docsDir = path.join(outputDir, 'docs');
|
||||||
|
const oauth2DocsPath = path.join(docsDir, 'oauth2-configuration.md');
|
||||||
|
|
||||||
try {
|
// Web server files (if requested)
|
||||||
// Use the correct inputSpec variable
|
const webServerPath = path.join(srcDir, 'web-server.ts');
|
||||||
console.error(`Parsing OpenAPI spec: ${inputSpec}`);
|
const publicDir = path.join(outputDir, 'public');
|
||||||
const api = await SwaggerParser.dereference(inputSpec) as OpenAPIV3.Document;
|
const indexHtmlPath = path.join(publicDir, 'index.html');
|
||||||
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 {
|
try {
|
||||||
await fs.rm(outputDir, { recursive: true, force: true });
|
// Check if output directory exists and is not empty
|
||||||
console.error(`Cleaned up partially created directory: ${outputDir}`);
|
if (!options.force) {
|
||||||
} catch (cleanupError) {
|
try {
|
||||||
console.error(`Failed to cleanup directory ${outputDir}:`, cleanupError);
|
const dirExists = await fs.stat(outputDir).catch(() => false);
|
||||||
|
if (dirExists) {
|
||||||
|
const files = await fs.readdir(outputDir);
|
||||||
|
if (files.length > 0) {
|
||||||
|
console.error(`Error: Output directory ${outputDir} already exists and is not empty.`);
|
||||||
|
console.error('Use --force to overwrite existing files.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// Directory doesn't exist, which is fine
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse OpenAPI spec
|
||||||
|
console.error(`Parsing OpenAPI spec: ${inputSpec}`);
|
||||||
|
const api = await SwaggerParser.dereference(inputSpec) as OpenAPIV3.Document;
|
||||||
|
console.error('OpenAPI spec parsed successfully.');
|
||||||
|
|
||||||
|
// Determine server name and version
|
||||||
|
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...');
|
||||||
|
const serverTsContent = generateMcpServerCode(api, options, serverName, serverVersion);
|
||||||
|
|
||||||
|
console.error('Generating package.json...');
|
||||||
|
const packageJsonContent = generatePackageJson(serverName, serverVersion, options.transport === 'web');
|
||||||
|
|
||||||
|
console.error('Generating tsconfig.json...');
|
||||||
|
const tsconfigJsonContent = generateTsconfigJson();
|
||||||
|
|
||||||
|
console.error('Generating .gitignore...');
|
||||||
|
const gitignoreContent = generateGitignore();
|
||||||
|
|
||||||
|
console.error('Generating ESLint config...');
|
||||||
|
const eslintConfigContent = generateEslintConfig();
|
||||||
|
|
||||||
|
console.error('Generating Prettier config...');
|
||||||
|
const prettierConfigContent = generatePrettierConfig();
|
||||||
|
|
||||||
|
console.error('Generating Jest config...');
|
||||||
|
const jestConfigContent = generateJestConfig();
|
||||||
|
|
||||||
|
console.error('Generating .env.example file...');
|
||||||
|
const envExampleContent = generateEnvExample(api.components?.securitySchemes);
|
||||||
|
|
||||||
|
console.error('Generating OAuth2 documentation...');
|
||||||
|
const oauth2DocsContent = generateOAuth2Docs(api.components?.securitySchemes);
|
||||||
|
|
||||||
|
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}`);
|
||||||
|
|
||||||
|
await fs.writeFile(eslintPath, eslintConfigContent);
|
||||||
|
console.error(` -> Created ${eslintPath}`);
|
||||||
|
|
||||||
|
await fs.writeFile(prettierPath, prettierConfigContent);
|
||||||
|
console.error(` -> Created ${prettierPath}`);
|
||||||
|
|
||||||
|
await fs.writeFile(jestConfigPath, jestConfigContent);
|
||||||
|
console.error(` -> Created ${jestConfigPath}`);
|
||||||
|
|
||||||
|
await fs.writeFile(envExamplePath, envExampleContent);
|
||||||
|
console.error(` -> Created ${envExamplePath}`);
|
||||||
|
|
||||||
|
// Only write OAuth2 docs if there are OAuth2 security schemes
|
||||||
|
if (oauth2DocsContent.includes("No OAuth2 security schemes defined")) {
|
||||||
|
console.error(` -> No OAuth2 security schemes found, skipping documentation`);
|
||||||
|
} else {
|
||||||
|
await fs.mkdir(docsDir, { recursive: true });
|
||||||
|
await fs.writeFile(oauth2DocsPath, oauth2DocsContent);
|
||||||
|
console.error(` -> Created ${oauth2DocsPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate web server files if web transport is requested
|
||||||
|
if (options.transport === 'web') {
|
||||||
|
console.error('Generating web server files...');
|
||||||
|
|
||||||
|
// Generate web server code
|
||||||
|
const webServerCode = generateWebServerCode(options.port || 3000);
|
||||||
|
await fs.writeFile(webServerPath, webServerCode);
|
||||||
|
console.error(` -> Created ${webServerPath}`);
|
||||||
|
|
||||||
|
// Create public directory and index.html
|
||||||
|
await fs.mkdir(publicDir, { recursive: true });
|
||||||
|
|
||||||
|
// Generate test client
|
||||||
|
const indexHtmlContent = generateTestClientHtml(serverName);
|
||||||
|
await fs.writeFile(indexHtmlPath, indexHtmlContent);
|
||||||
|
console.error(` -> Created ${indexHtmlPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
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`);
|
||||||
|
|
||||||
|
if (options.transport === 'web') {
|
||||||
|
console.error(`3. Build the TypeScript code: npm run build`);
|
||||||
|
console.error(`4. Run the server in web mode: npm run start:web`);
|
||||||
|
console.error(` (This will start a web server on port ${options.port || 3000})`);
|
||||||
|
console.error(` Access the test client at: http://localhost:${options.port || 3000}`);
|
||||||
|
} else {
|
||||||
|
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);
|
||||||
|
|
||||||
|
// Only attempt cleanup if the directory exists and force option was used
|
||||||
|
if (options.force) {
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
main();
|
main().catch(error => {
|
||||||
|
console.error('Unhandled error:', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
219
src/parser/extract-tools.ts
Normal file
219
src/parser/extract-tools.ts
Normal file
@ -0,0 +1,219 @@
|
|||||||
|
/**
|
||||||
|
* Functions for extracting tools from an OpenAPI specification
|
||||||
|
*/
|
||||||
|
import { OpenAPIV3 } from 'openapi-types';
|
||||||
|
import type { JSONSchema7, JSONSchema7TypeName } from 'json-schema';
|
||||||
|
import { generateOperationId } from '../utils/code-gen.js';
|
||||||
|
import { McpToolDefinition } from '../types/index.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts tool definitions from an OpenAPI document
|
||||||
|
*
|
||||||
|
* @param api OpenAPI document
|
||||||
|
* @returns Array of MCP tool definitions
|
||||||
|
*/
|
||||||
|
export function extractToolsFromApi(api: OpenAPIV3.Document): McpToolDefinition[] {
|
||||||
|
const tools: McpToolDefinition[] = [];
|
||||||
|
const usedNames = new Set<string>();
|
||||||
|
const globalSecurity = api.security || [];
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
// Generate a unique name for the tool
|
||||||
|
let baseName = operation.operationId || generateOperationId(method, path);
|
||||||
|
if (!baseName) continue;
|
||||||
|
|
||||||
|
// Sanitize the name to be MCP-compatible (only a-z, 0-9, _, -)
|
||||||
|
baseName = baseName.replace(/\./g, '_').replace(/[^a-z0-9_-]/gi, '_').toLowerCase();
|
||||||
|
|
||||||
|
let finalToolName = baseName;
|
||||||
|
let counter = 1;
|
||||||
|
while (usedNames.has(finalToolName)) {
|
||||||
|
finalToolName = `${baseName}_${counter++}`;
|
||||||
|
}
|
||||||
|
usedNames.add(finalToolName);
|
||||||
|
|
||||||
|
// Get or create a description
|
||||||
|
const description = operation.description || operation.summary ||
|
||||||
|
`Executes ${method.toUpperCase()} ${path}`;
|
||||||
|
|
||||||
|
// Generate input schema and extract parameters
|
||||||
|
const { inputSchema, parameters, requestBodyContentType } = generateInputSchemaAndDetails(operation);
|
||||||
|
|
||||||
|
// Extract parameter details for execution
|
||||||
|
const executionParameters = parameters.map(p => ({ name: p.name, in: p.in }));
|
||||||
|
|
||||||
|
// Determine security requirements
|
||||||
|
const securityRequirements = operation.security === null ?
|
||||||
|
globalSecurity :
|
||||||
|
operation.security || globalSecurity;
|
||||||
|
|
||||||
|
// Create the tool definition
|
||||||
|
tools.push({
|
||||||
|
name: finalToolName,
|
||||||
|
description,
|
||||||
|
inputSchema,
|
||||||
|
method,
|
||||||
|
pathTemplate: path,
|
||||||
|
parameters,
|
||||||
|
executionParameters,
|
||||||
|
requestBodyContentType,
|
||||||
|
securityRequirements,
|
||||||
|
operationId: baseName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tools;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates input schema and extracts parameter details from an operation
|
||||||
|
*
|
||||||
|
* @param operation OpenAPI operation object
|
||||||
|
* @returns Input schema, parameters, and request body content type
|
||||||
|
*/
|
||||||
|
export function generateInputSchemaAndDetails(operation: OpenAPIV3.OperationObject): {
|
||||||
|
inputSchema: JSONSchema7 | boolean;
|
||||||
|
parameters: OpenAPIV3.ParameterObject[];
|
||||||
|
requestBodyContentType?: string;
|
||||||
|
} {
|
||||||
|
const properties: { [key: string]: JSONSchema7 | boolean } = {};
|
||||||
|
const required: string[] = [];
|
||||||
|
|
||||||
|
// Process parameters
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Process request body (if present)
|
||||||
|
let requestBodyContentType: string | undefined = undefined;
|
||||||
|
|
||||||
|
if (operation.requestBody) {
|
||||||
|
const opRequestBody = operation.requestBody as OpenAPIV3.RequestBodyObject;
|
||||||
|
const jsonContent = opRequestBody.content?.['application/json'];
|
||||||
|
const firstContent = opRequestBody.content ? Object.entries(opRequestBody.content)[0] : undefined;
|
||||||
|
|
||||||
|
if (jsonContent?.schema) {
|
||||||
|
requestBodyContentType = 'application/json';
|
||||||
|
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 if (firstContent) {
|
||||||
|
const [contentType] = firstContent;
|
||||||
|
requestBodyContentType = contentType;
|
||||||
|
|
||||||
|
properties['requestBody'] = {
|
||||||
|
type: 'string',
|
||||||
|
description: opRequestBody.description || `Request body (content type: ${contentType})`
|
||||||
|
};
|
||||||
|
|
||||||
|
if (opRequestBody.required) required.push('requestBody');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combine everything into a JSON Schema
|
||||||
|
const inputSchema: JSONSchema7 = {
|
||||||
|
type: 'object',
|
||||||
|
properties,
|
||||||
|
...(required.length > 0 && { required })
|
||||||
|
};
|
||||||
|
|
||||||
|
return { inputSchema, parameters: allParameters, requestBodyContentType };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps an OpenAPI schema to a JSON Schema
|
||||||
|
*
|
||||||
|
* @param schema OpenAPI schema object or reference
|
||||||
|
* @returns JSON Schema representation
|
||||||
|
*/
|
||||||
|
export function mapOpenApiSchemaToJsonSchema(schema: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject): JSONSchema7 | boolean {
|
||||||
|
// Handle reference objects
|
||||||
|
if ('$ref' in schema) {
|
||||||
|
console.warn(`Unresolved $ref '${schema.$ref}'.`);
|
||||||
|
return { type: 'object' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle boolean schemas
|
||||||
|
if (typeof schema === 'boolean') return schema;
|
||||||
|
|
||||||
|
// Create a copy of the schema to modify
|
||||||
|
const jsonSchema: JSONSchema7 = { ...schema } as any;
|
||||||
|
|
||||||
|
// Convert integer type to number (JSON Schema compatible)
|
||||||
|
if (schema.type === 'integer') jsonSchema.type = 'number';
|
||||||
|
|
||||||
|
// Remove OpenAPI-specific properties that aren't in JSON Schema
|
||||||
|
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;
|
||||||
|
|
||||||
|
// Handle nullable properties by adding null to the type
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recursively process object properties
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recursively process array items
|
||||||
|
if (jsonSchema.type === 'array' && typeof jsonSchema.items === 'object' && jsonSchema.items !== null) {
|
||||||
|
jsonSchema.items = mapOpenApiSchemaToJsonSchema(
|
||||||
|
jsonSchema.items as OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonSchema;
|
||||||
|
}
|
||||||
4
src/parser/index.ts
Normal file
4
src/parser/index.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
/**
|
||||||
|
* Parser module exports
|
||||||
|
*/
|
||||||
|
export * from './extract-tools.js';
|
||||||
57
src/types/index.ts
Normal file
57
src/types/index.ts
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
/**
|
||||||
|
* Core type definitions for the openapi-to-mcp generator
|
||||||
|
*/
|
||||||
|
import { OpenAPIV3 } from 'openapi-types';
|
||||||
|
import type { JSONSchema7, JSONSchema7TypeName } from 'json-schema';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CLI options for the generator
|
||||||
|
*/
|
||||||
|
export interface CliOptions {
|
||||||
|
/** Path to the OpenAPI specification file */
|
||||||
|
input: string;
|
||||||
|
/** Output directory path for generated files */
|
||||||
|
output: string;
|
||||||
|
/** Optional server name to override the one in the OpenAPI spec */
|
||||||
|
serverName?: string;
|
||||||
|
/** Optional server version to override the one in the OpenAPI spec */
|
||||||
|
serverVersion?: string;
|
||||||
|
/** Optional base URL to override the one in the OpenAPI spec */
|
||||||
|
baseUrl?: string;
|
||||||
|
/** Server transport type (stdio or web) */
|
||||||
|
transport?: 'stdio' | 'web';
|
||||||
|
/** Server port (for web transport) */
|
||||||
|
port?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MCP Tool Definition describes a tool extracted from an OpenAPI spec
|
||||||
|
* for use in Model Context Protocol server
|
||||||
|
*/
|
||||||
|
export interface McpToolDefinition {
|
||||||
|
/** Name of the tool, must be unique */
|
||||||
|
name: string;
|
||||||
|
/** Human-readable description of the tool */
|
||||||
|
description: string;
|
||||||
|
/** JSON Schema that defines the input parameters */
|
||||||
|
inputSchema: JSONSchema7 | boolean;
|
||||||
|
/** HTTP method for the operation (get, post, etc.) */
|
||||||
|
method: string;
|
||||||
|
/** URL path template with parameter placeholders */
|
||||||
|
pathTemplate: string;
|
||||||
|
/** OpenAPI parameter objects for this operation */
|
||||||
|
parameters: OpenAPIV3.ParameterObject[];
|
||||||
|
/** Parameter names and locations for execution */
|
||||||
|
executionParameters: { name: string, in: string }[];
|
||||||
|
/** Content type for request body, if applicable */
|
||||||
|
requestBodyContentType?: string;
|
||||||
|
/** Security requirements for this operation */
|
||||||
|
securityRequirements: OpenAPIV3.SecurityRequirementObject[];
|
||||||
|
/** Original operation ID from the OpenAPI spec */
|
||||||
|
operationId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper type for JSON objects
|
||||||
|
*/
|
||||||
|
export type JsonObject = Record<string, any>;
|
||||||
40
src/utils.ts
40
src/utils.ts
@ -1,40 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
154
src/utils/code-gen.ts
Normal file
154
src/utils/code-gen.ts
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
/**
|
||||||
|
* Code generation utilities for OpenAPI to MCP generator
|
||||||
|
*/
|
||||||
|
import { McpToolDefinition } from '../types/index.js';
|
||||||
|
import { OpenAPIV3 } from 'openapi-types';
|
||||||
|
import { sanitizeForTemplate } from './helpers.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates the tool definition map code
|
||||||
|
*
|
||||||
|
* @param tools List of tool definitions
|
||||||
|
* @param securitySchemes Security schemes from OpenAPI spec
|
||||||
|
* @returns Generated code for the tool definition map
|
||||||
|
*/
|
||||||
|
export function generateToolDefinitionMap(
|
||||||
|
tools: McpToolDefinition[],
|
||||||
|
securitySchemes?: OpenAPIV3.ComponentsObject['securitySchemes']
|
||||||
|
): string {
|
||||||
|
if (tools.length === 0) return "";
|
||||||
|
|
||||||
|
return tools.map(tool => {
|
||||||
|
// Safely stringify complex objects
|
||||||
|
let schemaString;
|
||||||
|
try {
|
||||||
|
schemaString = JSON.stringify(tool.inputSchema);
|
||||||
|
} catch (e) {
|
||||||
|
schemaString = '{}';
|
||||||
|
console.warn(`Failed to stringify schema for tool ${tool.name}: ${e}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let execParamsString;
|
||||||
|
try {
|
||||||
|
execParamsString = JSON.stringify(tool.executionParameters);
|
||||||
|
} catch (e) {
|
||||||
|
execParamsString = "[]";
|
||||||
|
console.warn(`Failed to stringify execution parameters for tool ${tool.name}: ${e}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let securityReqsString;
|
||||||
|
try {
|
||||||
|
securityReqsString = JSON.stringify(tool.securityRequirements);
|
||||||
|
} catch (e) {
|
||||||
|
securityReqsString = "[]";
|
||||||
|
console.warn(`Failed to stringify security requirements for tool ${tool.name}: ${e}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanitize description for template literal
|
||||||
|
const escapedDescription = sanitizeForTemplate(tool.description);
|
||||||
|
|
||||||
|
// Build the tool definition entry
|
||||||
|
return `
|
||||||
|
["${tool.name}", {
|
||||||
|
name: "${tool.name}",
|
||||||
|
description: \`${escapedDescription}\`,
|
||||||
|
inputSchema: ${schemaString},
|
||||||
|
method: "${tool.method}",
|
||||||
|
pathTemplate: "${tool.pathTemplate}",
|
||||||
|
executionParameters: ${execParamsString},
|
||||||
|
requestBodyContentType: ${tool.requestBodyContentType ? `"${tool.requestBodyContentType}"` : 'undefined'},
|
||||||
|
securityRequirements: ${securityReqsString}
|
||||||
|
}],`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates the list tools handler code
|
||||||
|
*
|
||||||
|
* @returns Generated code for the list tools handler
|
||||||
|
*/
|
||||||
|
export function generateListToolsHandler(): string {
|
||||||
|
return `
|
||||||
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||||
|
const toolsForClient: Tool[] = Array.from(toolDefinitionMap.values()).map(def => ({
|
||||||
|
name: def.name,
|
||||||
|
description: def.description,
|
||||||
|
inputSchema: def.inputSchema
|
||||||
|
}));
|
||||||
|
return { tools: toolsForClient };
|
||||||
|
});
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates the call tool handler code
|
||||||
|
*
|
||||||
|
* @returns Generated code for the call tool handler
|
||||||
|
*/
|
||||||
|
export function generateCallToolHandler(): string {
|
||||||
|
return `
|
||||||
|
server.setRequestHandler(CallToolRequestSchema, async (request: CallToolRequest): Promise<CallToolResult> => {
|
||||||
|
const { name: toolName, arguments: toolArgs } = request.params;
|
||||||
|
const toolDefinition = toolDefinitionMap.get(toolName);
|
||||||
|
if (!toolDefinition) {
|
||||||
|
console.error(\`Error: Unknown tool requested: \${toolName}\`);
|
||||||
|
return { content: [{ type: "text", text: \`Error: Unknown tool requested: \${toolName}\` }] };
|
||||||
|
}
|
||||||
|
return await executeApiTool(toolName, toolDefinition, toolArgs ?? {}, securitySchemes);
|
||||||
|
});
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a string to title case
|
||||||
|
*
|
||||||
|
* @param str String to convert
|
||||||
|
* @returns Title case string
|
||||||
|
*/
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates an operation ID from method and path
|
||||||
|
*
|
||||||
|
* @param method HTTP method
|
||||||
|
* @param path API path
|
||||||
|
* @returns Generated operation ID
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
}
|
||||||
112
src/utils/helpers.ts
Normal file
112
src/utils/helpers.ts
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
/**
|
||||||
|
* General helper utilities for OpenAPI to MCP generator
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Safely stringify a JSON object with proper error handling
|
||||||
|
*
|
||||||
|
* @param obj Object to stringify
|
||||||
|
* @param defaultValue Default value to return if stringify fails
|
||||||
|
* @returns JSON string or default value
|
||||||
|
*/
|
||||||
|
export function safeJsonStringify(obj: any, defaultValue: string = '{}'): string {
|
||||||
|
try {
|
||||||
|
return JSON.stringify(obj);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`Failed to stringify object: ${e}`);
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanitizes a string for use in template strings
|
||||||
|
*
|
||||||
|
* @param str String to sanitize
|
||||||
|
* @returns Sanitized string safe for use in template literals
|
||||||
|
*/
|
||||||
|
export function sanitizeForTemplate(str: string): string {
|
||||||
|
return (str || '').replace(/\\/g, '\\\\').replace(/`/g, '\\`');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a string to camelCase
|
||||||
|
*
|
||||||
|
* @param str String to convert
|
||||||
|
* @returns camelCase string
|
||||||
|
*/
|
||||||
|
export function toCamelCase(str: string): string {
|
||||||
|
return str
|
||||||
|
.replace(/(?:^\w|[A-Z]|\b\w)/g, (word, index) =>
|
||||||
|
index === 0 ? word.toLowerCase() : word.toUpperCase()
|
||||||
|
)
|
||||||
|
.replace(/\s+/g, '')
|
||||||
|
.replace(/[^a-zA-Z0-9]/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a string to PascalCase
|
||||||
|
*
|
||||||
|
* @param str String to convert
|
||||||
|
* @returns PascalCase string
|
||||||
|
*/
|
||||||
|
export function toPascalCase(str: string): string {
|
||||||
|
return str
|
||||||
|
.replace(/(?:^\w|[A-Z]|\b\w)/g, (word) => word.toUpperCase())
|
||||||
|
.replace(/\s+/g, '')
|
||||||
|
.replace(/[^a-zA-Z0-9]/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a valid variable name from a string
|
||||||
|
*
|
||||||
|
* @param str Input string
|
||||||
|
* @returns Valid JavaScript variable name
|
||||||
|
*/
|
||||||
|
export function toValidVariableName(str: string): string {
|
||||||
|
// Replace non-alphanumeric characters with underscores
|
||||||
|
const sanitized = str.replace(/[^a-zA-Z0-9_$]/g, '_');
|
||||||
|
|
||||||
|
// Ensure the variable name doesn't start with a number
|
||||||
|
return sanitized.match(/^[0-9]/) ? '_' + sanitized : sanitized;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a string is a valid JavaScript identifier
|
||||||
|
*
|
||||||
|
* @param str String to check
|
||||||
|
* @returns True if valid identifier, false otherwise
|
||||||
|
*/
|
||||||
|
export function isValidIdentifier(str: string): boolean {
|
||||||
|
// Check if the string is a valid JavaScript identifier
|
||||||
|
return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats a string for use in code comments
|
||||||
|
*
|
||||||
|
* @param str String to format
|
||||||
|
* @param maxLineLength Maximum line length
|
||||||
|
* @returns Formatted comment string
|
||||||
|
*/
|
||||||
|
export function formatComment(str: string, maxLineLength: number = 80): string {
|
||||||
|
if (!str) return '';
|
||||||
|
|
||||||
|
const words = str.trim().split(/\s+/);
|
||||||
|
const lines: string[] = [];
|
||||||
|
let currentLine = '';
|
||||||
|
|
||||||
|
words.forEach(word => {
|
||||||
|
if ((currentLine + ' ' + word).length <= maxLineLength) {
|
||||||
|
currentLine += (currentLine ? ' ' : '') + word;
|
||||||
|
} else {
|
||||||
|
lines.push(currentLine);
|
||||||
|
currentLine = word;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (currentLine) {
|
||||||
|
lines.push(currentLine);
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines.join('\n * ');
|
||||||
|
}
|
||||||
7
src/utils/index.ts
Normal file
7
src/utils/index.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* Utilities module exports
|
||||||
|
*/
|
||||||
|
export * from './code-gen.js';
|
||||||
|
export * from './security.js';
|
||||||
|
export * from './helpers.js';
|
||||||
|
export { determineBaseUrl } from './url.js';
|
||||||
584
src/utils/security.ts
Normal file
584
src/utils/security.ts
Normal file
File diff suppressed because it is too large
Load Diff
101
src/utils/url.ts
Normal file
101
src/utils/url.ts
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
/**
|
||||||
|
* URL handling utilities for OpenAPI to MCP generator
|
||||||
|
*/
|
||||||
|
import { OpenAPIV3 } from 'openapi-types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines the base URL from the OpenAPI document or CLI options
|
||||||
|
*
|
||||||
|
* @param api OpenAPI document
|
||||||
|
* @param cmdLineBaseUrl Optional base URL from command line options
|
||||||
|
* @returns The determined base URL or null if none is available
|
||||||
|
*/
|
||||||
|
export function determineBaseUrl(api: OpenAPIV3.Document, cmdLineBaseUrl?: string): string | null {
|
||||||
|
// Command line option takes precedence
|
||||||
|
if (cmdLineBaseUrl) {
|
||||||
|
return normalizeUrl(cmdLineBaseUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Single server in OpenAPI spec
|
||||||
|
if (api.servers && api.servers.length === 1 && api.servers[0].url) {
|
||||||
|
return normalizeUrl(api.servers[0].url);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multiple servers - use first one with warning
|
||||||
|
if (api.servers && api.servers.length > 1) {
|
||||||
|
console.warn(`Multiple servers found. Using first: "${api.servers[0].url}". Use --base-url to override.`);
|
||||||
|
return normalizeUrl(api.servers[0].url);
|
||||||
|
}
|
||||||
|
|
||||||
|
// No server information available
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes a URL by removing trailing slashes
|
||||||
|
*
|
||||||
|
* @param url URL to normalize
|
||||||
|
* @returns Normalized URL
|
||||||
|
*/
|
||||||
|
export function normalizeUrl(url: string): string {
|
||||||
|
return url.replace(/\/$/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Joins URL segments handling slashes correctly
|
||||||
|
*
|
||||||
|
* @param baseUrl Base URL
|
||||||
|
* @param path Path to append
|
||||||
|
* @returns Joined URL
|
||||||
|
*/
|
||||||
|
export function joinUrl(baseUrl: string, path: string): string {
|
||||||
|
if (!baseUrl) return path;
|
||||||
|
if (!path) return baseUrl;
|
||||||
|
|
||||||
|
const normalizedBase = normalizeUrl(baseUrl);
|
||||||
|
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
|
||||||
|
|
||||||
|
return `${normalizedBase}${normalizedPath}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a URL with query parameters
|
||||||
|
*
|
||||||
|
* @param baseUrl Base URL
|
||||||
|
* @param queryParams Query parameters
|
||||||
|
* @returns URL with query parameters
|
||||||
|
*/
|
||||||
|
export function buildUrlWithQuery(baseUrl: string, queryParams: Record<string, any>): string {
|
||||||
|
if (!Object.keys(queryParams).length) return baseUrl;
|
||||||
|
|
||||||
|
const url = new URL(baseUrl.startsWith('http') ? baseUrl : `http://localhost${baseUrl.startsWith('/') ? '' : '/'}${baseUrl}`);
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(queryParams)) {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
value.forEach(item => url.searchParams.append(key, String(item)));
|
||||||
|
} else {
|
||||||
|
url.searchParams.append(key, String(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove http://localhost if we added it
|
||||||
|
return baseUrl.startsWith('http') ? url.toString() : url.pathname + url.search;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts path parameters from a URL template
|
||||||
|
*
|
||||||
|
* @param urlTemplate URL template with {param} placeholders
|
||||||
|
* @returns Array of parameter names
|
||||||
|
*/
|
||||||
|
export function extractPathParams(urlTemplate: string): string[] {
|
||||||
|
const paramRegex = /{([^}]+)}/g;
|
||||||
|
const params: string[] = [];
|
||||||
|
let match;
|
||||||
|
|
||||||
|
while ((match = paramRegex.exec(urlTemplate)) !== null) {
|
||||||
|
params.push(match[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return params;
|
||||||
|
}
|
||||||
@ -1,15 +1,18 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2022",
|
"target": "ES2022",
|
||||||
"module": "Node16",
|
"module": "NodeNext",
|
||||||
"moduleResolution": "Node16",
|
"moduleResolution": "NodeNext",
|
||||||
"outDir": "./build",
|
"esModuleInterop": true,
|
||||||
"rootDir": "./src",
|
"forceConsistentCasingInFileNames": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"esModuleInterop": true,
|
"skipLibCheck": true,
|
||||||
"skipLibCheck": true,
|
"declaration": true,
|
||||||
"forceConsistentCasingInFileNames": true
|
"sourceMap": true,
|
||||||
},
|
"outDir": "./dist",
|
||||||
"include": ["src/**/*"],
|
"rootDir": "./src",
|
||||||
"exclude": ["node_modules"]
|
"resolveJsonModule": true
|
||||||
}
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist", "test"]
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user