# Directory Structure ``` ├── .env.example ├── .gitignore ├── config.json ├── examples │ └── swagger-pet-store.json ├── jest.config.js ├── LICENSE ├── package.json ├── README.md ├── src │ ├── config.ts │ ├── mcp-server.ts │ ├── server.ts │ ├── types │ │ └── index.ts │ └── types.ts ├── tests │ └── test-pets.js ├── tsconfig.json └── yarn.lock ``` # Files -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- ``` 1 | # Server Configuration 2 | PORT=3000 3 | 4 | # API Authentication 5 | API_USERNAME= 6 | API_PASSWORD= 7 | API_TOKEN= 8 | 9 | # Default API Configuration 10 | DEFAULT_API_BASE_URL= 11 | DEFAULT_SWAGGER_URL= ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | # Dependencies 2 | node_modules/ 3 | yarn-debug.log* 4 | yarn-error.log* 5 | 6 | # Build output 7 | dist/ 8 | build/ 9 | 10 | # Environment variables 11 | .env 12 | .env.local 13 | .env.*.local 14 | 15 | # IDE and editor files 16 | .idea/ 17 | .vscode/ 18 | *.swp 19 | *.swo 20 | .DS_Store 21 | 22 | # Test coverage 23 | coverage/ 24 | 25 | # Logs 26 | logs/ 27 | *.log 28 | npm-debug.log* ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | # Swagger MCP Server 2 | 3 | A server that ingests and serves Swagger/OpenAPI specifications through the Model Context Protocol (MCP). 4 | 5 | ## Features 6 | 7 | - Loads Swagger/OpenAPI specifications 8 | - Supports multiple authentication methods: 9 | - Basic Auth 10 | - Bearer Token 11 | - API Key (header or query) 12 | - OAuth2 13 | - Automatically generates MCP tools from API endpoints 14 | - Server-Sent Events (SSE) support for real-time communication 15 | - TypeScript support 16 | 17 | ## Security 18 | 19 | This is a personal server!! Do not expose it to the public internet. 20 | If the underlying API requires authentication, you should not expose the MCP server to the public internet. 21 | 22 | ## TODO 23 | 24 | - secrets - the MCP server should be able to use secrets from the user to authenticate requests to the API 25 | - Comprehensive test suite 26 | 27 | ## Prerequisites 28 | 29 | - Node.js (v18 or higher) 30 | - Yarn package manager 31 | - TypeScript 32 | 33 | ## Installation 34 | 35 | 1. Clone the repository: 36 | ```bash 37 | git clone https://github.com/dcolley/swagger-mcp.git 38 | cd swagger-mcp 39 | ``` 40 | 41 | 2. Install dependencies: 42 | ```bash 43 | yarn install 44 | ``` 45 | 46 | 3. Create a `.env` file based on the example: 47 | ```bash 48 | cp .env.example .env 49 | ``` 50 | 51 | 4. Configure your Swagger/OpenAPI specification: 52 | - Place your Swagger file in the project (e.g., `swagger.json`) 53 | - Or provide a URL to your Swagger specification 54 | 55 | 5. Update the configuration in `config.json` with your server settings: 56 | ```json 57 | { 58 | "server": { 59 | "host": "localhost", 60 | "port": 3000 61 | }, 62 | "swagger": { 63 | "url": "url-or-path/to/your/swagger.json", 64 | "apiBaseUrl": "https://api.example.com", // Fallback if not specified in Swagger 65 | "defaultAuth": { // Fallback if not specified in Swagger 66 | "type": "apiKey", 67 | "apiKey": "your-api-key", 68 | "apiKeyName": "api_key", 69 | "apiKeyIn": "header" 70 | } 71 | } 72 | } 73 | ``` 74 | 75 | Note: The server prioritizes settings from the Swagger specification over the config file: 76 | - If the Swagger file contains a `servers` array, the first server URL will be used as the base URL 77 | - If the Swagger file defines security schemes, they will be used for authentication 78 | - The config file settings serve as fallbacks when not specified in the Swagger file 79 | 80 | ## Usage 81 | 82 | 1. Start the development server: 83 | ```bash 84 | yarn dev 85 | ``` 86 | 87 | 2. Build for production: 88 | ```bash 89 | yarn build 90 | ``` 91 | 92 | 3. Start the production server: 93 | ```bash 94 | yarn start 95 | ``` 96 | 97 | ## API Endpoints 98 | 99 | - `GET /health` - Check server health status 100 | - `GET /sse` - Establish Server-Sent Events connection 101 | - `POST /messages` - Send messages to the MCP server 102 | 103 | ## Testing 104 | 105 | Run the test suite: 106 | ```bash 107 | # Run tests once 108 | yarn test 109 | 110 | # Run tests in watch mode 111 | yarn test:watch 112 | 113 | # Run tests with coverage report 114 | yarn test:coverage 115 | ``` 116 | 117 | ## Authentication 118 | 119 | The server supports various authentication methods. Configure them in the `config.json` file as fallbacks when not specified in the Swagger file: 120 | 121 | ### Basic Auth 122 | ```json 123 | { 124 | "defaultAuth": { 125 | "type": "basic", 126 | "username": "your-username", 127 | "password": "your-password" 128 | } 129 | } 130 | ``` 131 | 132 | ### Bearer Token 133 | ```json 134 | { 135 | "defaultAuth": { 136 | "type": "bearer", 137 | "token": "your-bearer-token" 138 | } 139 | } 140 | ``` 141 | 142 | ### API Key 143 | ```json 144 | { 145 | "defaultAuth": { 146 | "type": "apiKey", 147 | "apiKey": "your-api-key", 148 | "apiKeyName": "X-API-Key", 149 | "apiKeyIn": "header" 150 | } 151 | } 152 | ``` 153 | 154 | ### OAuth2 155 | ```json 156 | { 157 | "defaultAuth": { 158 | "type": "oauth2", 159 | "token": "your-oauth-token" 160 | } 161 | } 162 | ``` 163 | 164 | ## Development 165 | 166 | 1. Start the development server: 167 | ```bash 168 | yarn dev 169 | ``` 170 | 171 | <!-- 2. Make changes to the code 172 | 173 | 3. Run tests to ensure everything works: 174 | ```bash 175 | yarn test 176 | ``` 177 | 178 | 4. Build the project: 179 | ```bash 180 | yarn build 181 | ``` --> 182 | 183 | <!-- ## Contributing 184 | 185 | 1. Fork the repository 186 | 2. Create your feature branch (`git checkout -b feature/amazing-feature`) 187 | 3. Commit your changes (`git commit -m 'Add some amazing feature'`) 188 | 4. Push to the branch (`git push origin feature/amazing-feature`) 189 | 5. Open a Pull Request --> 190 | 191 | ## License 192 | 193 | This project is licensed under the Apache 2.0 License. 194 | 195 | ## Environment Variables 196 | 197 | - `PORT`: Server port (default: 3000) 198 | - `API_USERNAME`: Username for API authentication (fallback) 199 | - `API_PASSWORD`: Password for API authentication (fallback) 200 | - `API_TOKEN`: API token for authentication (fallback) 201 | - `DEFAULT_API_BASE_URL`: Default base URL for API endpoints (fallback) 202 | - `DEFAULT_SWAGGER_URL`: Default Swagger specification URL 203 | ``` -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- ```javascript 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | roots: ['<rootDir>/tests'], 5 | transform: { 6 | '^.+\\.tsx?$': 'ts-jest', 7 | }, 8 | testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$', 9 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], 10 | }; ``` -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "swagger": { 3 | "url": "https://petstore.swagger.io/v2/swagger.json", 4 | "apiBaseUrl": "https://petstore.swagger.io/v2", 5 | "defaultAuth": { 6 | "type": "apiKey", 7 | "apiKey": "special-key", 8 | "apiKeyName": "api_key", 9 | "apiKeyIn": "header" 10 | } 11 | }, 12 | "log": { 13 | "level": "info" 14 | }, 15 | "server": { 16 | "port": 3000, 17 | "host": "0.0.0.0" 18 | } 19 | } ``` -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- ```typescript 1 | export interface SwaggerConfig { 2 | swaggerUrl?: string; 3 | swaggerFile?: string; 4 | apiBaseUrl: string; 5 | auth?: AuthConfig; 6 | } 7 | 8 | export interface AuthConfig { 9 | type: 'basic' | 'bearer' | 'apiKey' | 'oauth2'; 10 | username?: string; 11 | password?: string; 12 | token?: string; 13 | apiKey?: string; 14 | apiKeyName?: string; 15 | apiKeyIn?: 'header' | 'query'; 16 | } 17 | 18 | export interface ToolInput { 19 | auth?: AuthConfig; 20 | [key: string]: any; 21 | } 22 | 23 | export interface SecurityScheme { 24 | type: string; 25 | description?: string; 26 | name?: string; 27 | in?: string; 28 | scheme?: string; 29 | flows?: { 30 | implicit?: { 31 | authorizationUrl: string; 32 | scopes: Record<string, string>; 33 | }; 34 | [key: string]: any; 35 | }; 36 | } ``` -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | export interface SwaggerConfig { 2 | swaggerFile?: string; 3 | swaggerUrl?: string; 4 | apiBaseUrl: string; 5 | auth?: AuthConfig; 6 | } 7 | 8 | export interface ServerConfig { 9 | port: number; 10 | username?: string; 11 | password?: string; 12 | token?: string; 13 | } 14 | 15 | export interface AuthConfig { 16 | type: 'basic' | 'bearer' | 'apiKey' | 'oauth2'; 17 | username?: string; 18 | password?: string; 19 | token?: string; 20 | apiKey?: string; 21 | apiKeyIn?: 'header' | 'query'; 22 | apiKeyName?: string; 23 | } 24 | 25 | export interface SecurityScheme { 26 | type: string; 27 | description?: string; 28 | name?: string; 29 | in?: string; 30 | scheme?: string; 31 | bearerFormat?: string; 32 | flows?: any; 33 | } 34 | 35 | export interface ToolInput { 36 | auth?: AuthConfig; 37 | [key: string]: any; 38 | } ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "swagger-mcp", 3 | "version": "1.0.0", 4 | "description": "Server that ingests and serves Swagger/OpenAPI specifications", 5 | "main": "dist/server.js", 6 | "scripts": { 7 | "start": "node dist/server.js", 8 | "dev": "NODE_OPTIONS='--loader ts-node/esm' ts-node src/server.ts", 9 | "build": "tsc", 10 | "watch": "tsc -w", 11 | "test": "jest", 12 | "test:watch": "jest --watch", 13 | "test:coverage": "jest --coverage" 14 | }, 15 | "keywords": [ 16 | "swagger", 17 | "openapi", 18 | "api", 19 | "documentation" 20 | ], 21 | "author": "", 22 | "license": "ISC", 23 | "dependencies": { 24 | "@apidevtools/swagger-parser": "^10.1.0", 25 | "@modelcontextprotocol/sdk": "^1.7.0", 26 | "@types/cors": "^2.8.17", 27 | "@types/express": "^5.0.0", 28 | "@types/swagger-parser": "^7.0.1", 29 | "axios": "^1.8.3", 30 | "cors": "^2.8.5", 31 | "dotenv": "^16.4.5", 32 | "eventsource": "^3.0.5", 33 | "express": "^4.18.3", 34 | "node-fetch": "^3.3.2", 35 | "openapi-types": "^12.1.3", 36 | "ts-node": "^10.9.2", 37 | "tslib": "^2.8.1", 38 | "typescript": "^5.4.2", 39 | "zod": "^3.22.4" 40 | }, 41 | "devDependencies": { 42 | "@types/jest": "^29.5.14", 43 | "@types/supertest": "^6.0.2", 44 | "jest": "^29.7.0", 45 | "supertest": "^7.0.0", 46 | "ts-jest": "^29.2.6" 47 | }, 48 | "packageManager": "[email protected]+sha1.1959a18351b811cdeedbd484a8f86c3cc3bbaf72" 49 | } 50 | ``` -------------------------------------------------------------------------------- /tests/test-pets.js: -------------------------------------------------------------------------------- ```javascript 1 | import fetch from 'node-fetch'; 2 | import { EventSource } from 'eventsource'; 3 | 4 | async function findPetsBySold() { 5 | try { 6 | // First check if server is ready 7 | const healthResponse = await fetch('http://localhost:3000/health'); 8 | const health = await healthResponse.json(); 9 | 10 | if (health.mcpServer !== 'initialized') { 11 | console.error('Server is not ready. Please wait for it to initialize.'); 12 | return; 13 | } 14 | 15 | // Create SSE connection 16 | const eventSource = new EventSource('http://localhost:3000/sse'); 17 | 18 | eventSource.onopen = async () => { 19 | console.log('SSE connection opened'); 20 | 21 | // Send the request 22 | try { 23 | const response = await fetch('http://localhost:3000/messages', { 24 | method: 'POST', 25 | headers: { 26 | 'Content-Type': 'application/json' 27 | }, 28 | body: JSON.stringify({ 29 | type: 'invoke', 30 | tool: 'findPetsByStatus', 31 | input: { 32 | status: ['sold'] 33 | } 34 | }) 35 | }); 36 | 37 | const data = await response.json(); 38 | console.log('Response:', data); 39 | eventSource.close(); 40 | } catch (error) { 41 | console.error('Error sending message:', error.message); 42 | eventSource.close(); 43 | } 44 | }; 45 | 46 | eventSource.onerror = (error) => { 47 | console.error('SSE error:', error); 48 | eventSource.close(); 49 | }; 50 | 51 | eventSource.onmessage = (event) => { 52 | console.log('Received:', event.data); 53 | }; 54 | } catch (error) { 55 | console.error('Error:', error.message); 56 | if (error.message.includes('ECONNREFUSED')) { 57 | console.log('Make sure the server is running on port 3000'); 58 | } 59 | } 60 | } 61 | 62 | // Run the test 63 | findPetsBySold(); ``` -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import fs from 'fs/promises'; 3 | import path from 'path'; 4 | 5 | // Define the configuration schema 6 | export const ConfigSchema = z.object({ 7 | swagger: z.object({ 8 | url: z.string().url(), 9 | apiBaseUrl: z.string().url(), 10 | defaultAuth: z.object({ 11 | type: z.enum(['basic', 'bearer', 'apiKey', 'oauth2']), 12 | token: z.string().optional(), 13 | username: z.string().optional(), 14 | password: z.string().optional(), 15 | apiKey: z.string().optional(), 16 | apiKeyName: z.string().optional(), 17 | apiKeyIn: z.enum(['header', 'query']).optional(), 18 | }).optional(), 19 | }), 20 | log: z.object({ 21 | level: z.enum(['debug', 'info', 'warn', 'error']), 22 | }), 23 | server: z.object({ 24 | port: z.number().default(3000), 25 | host: z.string().default('0.0.0.0'), 26 | }), 27 | }); 28 | 29 | export type Config = z.infer<typeof ConfigSchema>; 30 | 31 | const defaultConfig: Config = { 32 | swagger: { 33 | url: 'https://petstore.swagger.io/v2/swagger.json', 34 | apiBaseUrl: 'https://petstore.swagger.io/v2', 35 | defaultAuth: { 36 | type: 'apiKey', 37 | apiKey: 'special-key', 38 | apiKeyName: 'api_key', 39 | apiKeyIn: 'header', 40 | }, 41 | }, 42 | log: { 43 | level: 'info', 44 | }, 45 | server: { 46 | port: 3000, 47 | host: '0.0.0.0', 48 | }, 49 | }; 50 | 51 | export async function loadConfig(configPath?: string): Promise<Config> { 52 | try { 53 | // If no config path provided, create default config file 54 | if (!configPath) { 55 | configPath = path.join(process.cwd(), 'config.json'); 56 | // Check if config file exists, if not create it with default values 57 | try { 58 | await fs.access(configPath); 59 | } catch { 60 | await fs.writeFile(configPath, JSON.stringify(defaultConfig, null, 2)); 61 | console.log(`Created default configuration file at ${configPath}`); 62 | } 63 | } 64 | 65 | const configFile = await fs.readFile(configPath, 'utf-8'); 66 | const config = JSON.parse(configFile); 67 | return ConfigSchema.parse(config); 68 | } catch (error) { 69 | if (error instanceof z.ZodError) { 70 | console.error('Invalid configuration:', error.errors); 71 | } else { 72 | console.error('Error loading configuration:', error); 73 | } 74 | console.log('Using default configuration'); 75 | return defaultConfig; 76 | } 77 | } ``` -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- ```typescript 1 | import express, { Request, Response, Router } from 'express'; 2 | import cors from 'cors'; 3 | import dotenv from 'dotenv'; 4 | import { SwaggerMcpServer } from './mcp-server'; 5 | import { loadConfig } from './config'; 6 | 7 | // Load environment variables 8 | dotenv.config(); 9 | 10 | const app = express(); 11 | const router = Router(); 12 | let mcpServer: SwaggerMcpServer | null = null; 13 | 14 | // Middleware 15 | // app.use(cors()); 16 | // app.use(express.json()); 17 | 18 | // Routes 19 | const handleSSE = async (req: Request, res: Response) => { 20 | console.debug('SSE connection request received'); 21 | if (!mcpServer) { 22 | console.warn('MCP server not initialized - rejecting SSE connection'); 23 | res.status(400).json({ error: 'MCP server not initialized' }); 24 | return; 25 | } 26 | console.debug('Establishing SSE connection...'); 27 | mcpServer.handleSSE(res); 28 | }; 29 | 30 | const handleMessage = async (req: Request, res: Response) => { 31 | console.debug('Message received:', { 32 | method: req.method, 33 | path: req.path, 34 | body: req.body 35 | }); 36 | if (!mcpServer) { 37 | console.warn('MCP server not initialized - rejecting message'); 38 | res.status(400).json({ error: 'MCP server not initialized' }); 39 | return; 40 | } 41 | mcpServer.handleMessage(req, res); 42 | }; 43 | 44 | const handleHealth = (_req: Request, res: Response) => { 45 | console.debug('Health check request received'); 46 | res.json({ 47 | status: 'ok', 48 | mcpServer: mcpServer ? 'initialized' : 'not initialized' 49 | }); 50 | }; 51 | 52 | // // Register routes 53 | // router.get('/sse', handleSSE); 54 | // router.post('/messages', handleMessage); 55 | // router.get('/health', handleHealth); 56 | 57 | // Mount router 58 | // app.use('/', router); 59 | 60 | app.get('/sse', handleSSE); 61 | app.post('/messages', handleMessage); 62 | app.get('/health', handleHealth); 63 | 64 | // Initialize server 65 | async function initializeServer() { 66 | try { 67 | console.log('Starting server initialization...'); 68 | 69 | // Load configuration 70 | const config = await loadConfig(); 71 | // set app logging level 72 | process.env.LOG_LEVEL = config.log?.level || 'info'; 73 | 74 | console.debug('Configuration loaded:', { 75 | swaggerUrl: config.swagger.url, 76 | apiBaseUrl: config.swagger.apiBaseUrl, 77 | hasDefaultAuth: !!config.swagger.defaultAuth 78 | }); 79 | 80 | // Create and initialize MCP server 81 | console.log('Creating MCP server instance...'); 82 | mcpServer = new SwaggerMcpServer(config.swagger.apiBaseUrl, config.swagger.defaultAuth); 83 | 84 | console.log('Loading Swagger specification...'); 85 | await mcpServer.loadSwaggerSpec(config.swagger.url); 86 | console.debug('Swagger specification loaded successfully'); 87 | 88 | // Start the server 89 | app.listen(config.server.port, config.server.host, () => { 90 | console.log('Server initialization complete'); 91 | console.log(`Server is running on http://${config.server.host}:${config.server.port}`); 92 | console.log('Swagger specification loaded from:', config.swagger.url); 93 | console.log('API Base URL:', config.swagger.apiBaseUrl); 94 | }); 95 | } catch (error) { 96 | console.error('Failed to initialize server:', error); 97 | process.exit(1); 98 | } 99 | } 100 | 101 | // Start the server 102 | initializeServer(); ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "ES2020", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "libReplacement": true, /* Enable lib replacement. */ 18 | // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ 19 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 20 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 21 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 22 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 23 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 24 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 25 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 26 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 27 | 28 | /* Modules */ 29 | "module": "NodeNext", /* Specify what module code is generated. */ 30 | "moduleResolution": "NodeNext", /* Specify how TypeScript looks up a file from a given module specifier. */ 31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 34 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 35 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 37 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 38 | // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ 39 | // "rewriteRelativeImportExtensions": true, /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */ 40 | // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ 41 | // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ 42 | // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ 43 | // "noUncheckedSideEffectImports": true, /* Check side effect imports. */ 44 | // "resolveJsonModule": true, /* Enable importing .json files. */ 45 | // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ 46 | // "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */ 47 | 48 | /* JavaScript Support */ 49 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 50 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 51 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 52 | 53 | /* Emit */ 54 | "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 55 | "declarationMap": true, /* Create sourcemaps for d.ts files. */ 56 | "emitDeclarationOnly": false, /* Only output d.ts files and not JavaScript files. */ 57 | "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 58 | "noEmit": false, /* Disable emitting files from a compilation. */ 59 | "outDir": "dist", /* Specify an output folder for all emitted files. */ 60 | "removeComments": true, /* Disable emitting comments. */ 61 | "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 62 | "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 63 | "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 64 | "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 65 | "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 66 | "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 67 | "newLine": "crlf", /* Set the newline character for emitting files. */ 68 | "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 69 | "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 70 | "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 71 | "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 72 | 73 | /* Interop Constraints */ 74 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 75 | // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ 76 | // "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */ 77 | // "erasableSyntaxOnly": true, /* Do not allow runtime constructs that are not part of ECMAScript. */ 78 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 79 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 80 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 81 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 82 | 83 | /* Type Checking */ 84 | "strict": true, /* Enable all strict type-checking options. */ 85 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 86 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 87 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 88 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 89 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 90 | // "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */ 91 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 92 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 93 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 94 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 95 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 96 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 97 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 98 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 99 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 100 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 101 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 102 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 103 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 104 | 105 | /* Completeness */ 106 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 107 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 108 | }, 109 | "include": ["src/**/*"], 110 | "exclude": ["node_modules", "dist"] 111 | } 112 | ``` -------------------------------------------------------------------------------- /examples/swagger-pet-store.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "swagger":"2.0", 3 | "info":{ 4 | "description":"This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key `special-key` to test the authorization filters.","version":"1.0.7","title":"Swagger Petstore","termsOfService":"http://swagger.io/terms/","contact":{"email":"[email protected]"},"license":{"name":"Apache 2.0","url":"http://www.apache.org/licenses/LICENSE-2.0.html"} 5 | }, 6 | "host":"petstore.swagger.io", 7 | "basePath":"/v2", 8 | "tags":[{"name":"pet","description":"Everything about your Pets","externalDocs":{"description":"Find out more","url":"http://swagger.io"}},{"name":"store","description":"Access to Petstore orders"},{"name":"user","description":"Operations about user", 9 | "externalDocs":{"description":"Find out more about our store","url":"http://swagger.io"}}], 10 | "schemes":["https","http"], 11 | "paths":{ 12 | 13 | "/pet/{petId}/uploadImage":{"post":{"tags":["pet"],"summary":"uploads an image","description":"","operationId":"uploadFile","consumes":["multipart/form-data"],"produces":["application/json"],"parameters":[{"name":"petId","in":"path","description":"ID of pet to update","required":true,"type":"integer","format":"int64"},{"name":"additionalMetadata","in":"formData","description":"Additional data to pass to server","required":false,"type":"string"},{"name":"file","in":"formData","description":"file to upload","required":false,"type":"file"}],"responses":{"200":{"description":"successful operation","schema":{"$ref":"#/definitions/ApiResponse"}}},"security":[{"petstore_auth":["write:pets","read:pets"]}]}}, 14 | 15 | "/pet":{ 16 | "post":{"tags":["pet"],"summary":"Add a new pet to the store","description":"","operationId":"addPet","consumes":["application/json","application/xml"],"produces":["application/json","application/xml"],"parameters":[{"in":"body","name":"body","description":"Pet object that needs to be added to the store","required":true,"schema":{"$ref":"#/definitions/Pet"}}],"responses":{"405":{"description":"Invalid input"}},"security":[{"petstore_auth":["write:pets","read:pets"]}]},"put":{"tags":["pet"],"summary":"Update an existing pet","description":"","operationId":"updatePet","consumes":["application/json","application/xml"],"produces":["application/json","application/xml"],"parameters":[{"in":"body","name":"body","description":"Pet object that needs to be added to the store","required":true,"schema":{"$ref":"#/definitions/Pet"}}],"responses":{"400":{"description":"Invalid ID supplied"},"404":{"description":"Pet not found"},"405":{"description":"Validation exception"}},"security":[{"petstore_auth":["write:pets","read:pets"]}]} 17 | }, 18 | 19 | "/pet/findByStatus":{ 20 | "get":{"tags":["pet"],"summary":"Finds Pets by status","description":"Multiple status values can be provided with comma separated strings","operationId":"findPetsByStatus","produces":["application/json","application/xml"], 21 | "parameters":[ 22 | {"name":"status","in":"query","description":"Status values that need to be considered for filter","required":true,"type":"array","items":{"type":"string","enum":["available","pending","sold"],"default":"available"},"collectionFormat":"multi"} 23 | ], 24 | "responses":{ 25 | "200":{"description":"successful operation","schema":{"type":"array","items":{"$ref":"#/definitions/Pet"}}}, 26 | "400":{"description":"Invalid status value"} 27 | }, 28 | "security":[{"petstore_auth":["write:pets","read:pets"]} 29 | ] 30 | } 31 | }, 32 | 33 | "/pet/findByTags":{"get":{"tags":["pet"],"summary":"Finds Pets by tags","description":"Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.","operationId":"findPetsByTags","produces":["application/json","application/xml"],"parameters":[{"name":"tags","in":"query","description":"Tags to filter by","required":true,"type":"array","items":{"type":"string"},"collectionFormat":"multi"}],"responses":{"200":{"description":"successful operation","schema":{"type":"array","items":{"$ref":"#/definitions/Pet"}}},"400":{"description":"Invalid tag value"}},"security":[{"petstore_auth":["write:pets","read:pets"]}],"deprecated":true}}, 34 | 35 | "/pet/{petId}":{"get":{"tags":["pet"],"summary":"Find pet by ID","description":"Returns a single pet","operationId":"getPetById","produces":["application/json","application/xml"],"parameters":[{"name":"petId","in":"path","description":"ID of pet to return","required":true,"type":"integer","format":"int64"}],"responses":{"200":{"description":"successful operation","schema":{"$ref":"#/definitions/Pet"}},"400":{"description":"Invalid ID supplied"},"404":{"description":"Pet not found"}},"security":[{"api_key":[]}]},"post":{"tags":["pet"],"summary":"Updates a pet in the store with form data","description":"","operationId":"updatePetWithForm","consumes":["application/x-www-form-urlencoded"],"produces":["application/json","application/xml"],"parameters":[{"name":"petId","in":"path","description":"ID of pet that needs to be updated","required":true,"type":"integer","format":"int64"},{"name":"name","in":"formData","description":"Updated name of the pet","required":false,"type":"string"},{"name":"status","in":"formData","description":"Updated status of the pet","required":false,"type":"string"}],"responses":{"405":{"description":"Invalid input"}},"security":[{"petstore_auth":["write:pets","read:pets"]}]},"delete":{"tags":["pet"],"summary":"Deletes a pet","description":"","operationId":"deletePet","produces":["application/json","application/xml"],"parameters":[{"name":"api_key","in":"header","required":false,"type":"string"},{"name":"petId","in":"path","description":"Pet id to delete","required":true,"type":"integer","format":"int64"}],"responses":{"400":{"description":"Invalid ID supplied"},"404":{"description":"Pet not found"}},"security":[{"petstore_auth":["write:pets","read:pets"]}]}}, 36 | 37 | "/store/inventory":{"get":{"tags":["store"],"summary":"Returns pet inventories by status","description":"Returns a map of status codes to quantities","operationId":"getInventory","produces":["application/json"],"parameters":[],"responses":{"200":{"description":"successful operation","schema":{"type":"object","additionalProperties":{"type":"integer","format":"int32"}}}},"security":[{"api_key":[]}]}}, 38 | 39 | "/store/order":{"post":{"tags":["store"],"summary":"Place an order for a pet","description":"","operationId":"placeOrder","consumes":["application/json"],"produces":["application/json","application/xml"],"parameters":[{"in":"body","name":"body","description":"order placed for purchasing the pet","required":true,"schema":{"$ref":"#/definitions/Order"}}],"responses":{"200":{"description":"successful operation","schema":{"$ref":"#/definitions/Order"}},"400":{"description":"Invalid Order"}}}}, 40 | 41 | "/store/order/{orderId}":{"get":{"tags":["store"],"summary":"Find purchase order by ID","description":"For valid response try integer IDs with value >= 1 and <= 10. Other values will generated exceptions","operationId":"getOrderById","produces":["application/json","application/xml"],"parameters":[{"name":"orderId","in":"path","description":"ID of pet that needs to be fetched","required":true,"type":"integer","maximum":10,"minimum":1,"format":"int64"}],"responses":{"200":{"description":"successful operation","schema":{"$ref":"#/definitions/Order"}},"400":{"description":"Invalid ID supplied"},"404":{"description":"Order not found"}}},"delete":{"tags":["store"],"summary":"Delete purchase order by ID","description":"For valid response try integer IDs with positive integer value. Negative or non-integer values will generate API errors","operationId":"deleteOrder","produces":["application/json","application/xml"],"parameters":[{"name":"orderId","in":"path","description":"ID of the order that needs to be deleted","required":true,"type":"integer","minimum":1,"format":"int64"}],"responses":{"400":{"description":"Invalid ID supplied"},"404":{"description":"Order not found"}}}}, 42 | 43 | "/user/createWithList":{"post":{"tags":["user"],"summary":"Creates list of users with given input array","description":"","operationId":"createUsersWithListInput","consumes":["application/json"],"produces":["application/json","application/xml"],"parameters":[{"in":"body","name":"body","description":"List of user object","required":true,"schema":{"type":"array","items":{"$ref":"#/definitions/User"}}}],"responses":{"default":{"description":"successful operation"}}}}, 44 | 45 | "/user/{username}":{ 46 | "get":{"tags":["user"],"summary":"Get user by user name","description":"","operationId":"getUserByName","produces":["application/json","application/xml"],"parameters":[{"name":"username","in":"path","description":"The name that needs to be fetched. Use user1 for testing. ","required":true,"type":"string"}],"responses":{"200":{"description":"successful operation","schema":{"$ref":"#/definitions/User"}},"400":{"description":"Invalid username supplied"},"404":{"description":"User not found"}}}, 47 | "put":{"tags":["user"],"summary":"Updated user","description":"This can only be done by the logged in user.","operationId":"updateUser","consumes":["application/json"],"produces":["application/json","application/xml"],"parameters":[{"name":"username","in":"path","description":"name that need to be updated","required":true,"type":"string"},{"in":"body","name":"body","description":"Updated user object","required":true,"schema":{"$ref":"#/definitions/User"}}],"responses":{"400":{"description":"Invalid user supplied"},"404":{"description":"User not found"}}}, 48 | "delete":{"tags":["user"],"summary":"Delete user","description":"This can only be done by the logged in user.","operationId":"deleteUser","produces":["application/json","application/xml"],"parameters":[{"name":"username","in":"path","description":"The name that needs to be deleted","required":true,"type":"string"}],"responses":{"400":{"description":"Invalid username supplied"},"404":{"description":"User not found"}}}}, 49 | 50 | "/user/login":{ 51 | "get":{"tags":["user"],"summary":"Logs user into the system","description":"","operationId":"loginUser","produces":["application/json","application/xml"],"parameters":[{"name":"username","in":"query","description":"The user name for login","required":true,"type":"string"},{"name":"password","in":"query","description":"The password for login in clear text","required":true,"type":"string"}],"responses":{"200":{"description":"successful operation","headers":{"X-Expires-After":{"type":"string","format":"date-time","description":"date in UTC when token expires"},"X-Rate-Limit":{"type":"integer","format":"int32","description":"calls per hour allowed by the user"}},"schema":{"type":"string"}},"400":{"description":"Invalid username/password supplied"}}} 52 | }, 53 | 54 | "/user/logout":{ 55 | "get":{"tags":["user"],"summary":"Logs out current logged in user session","description":"","operationId":"logoutUser","produces":["application/json","application/xml"],"parameters":[],"responses":{"default":{"description":"successful operation"}}} 56 | }, 57 | 58 | "/user/createWithArray":{ 59 | "post":{"tags":["user"],"summary":"Creates list of users with given input array","description":"","operationId":"createUsersWithArrayInput","consumes":["application/json"],"produces":["application/json","application/xml"],"parameters":[{"in":"body","name":"body","description":"List of user object","required":true,"schema":{"type":"array","items":{"$ref":"#/definitions/User"}}}],"responses":{"default":{"description":"successful operation"}}} 60 | }, 61 | 62 | "/user":{ 63 | "post":{"tags":["user"],"summary":"Create user","description":"This can only be done by the logged in user.","operationId":"createUser","consumes":["application/json"],"produces":["application/json","application/xml"],"parameters":[{"in":"body","name":"body","description":"Created user object","required":true,"schema":{"$ref":"#/definitions/User"}}],"responses":{"default":{"description":"successful operation"}}} 64 | } 65 | }, 66 | 67 | "securityDefinitions":{ 68 | "api_key":{"type":"apiKey","name":"api_key","in":"header"}, 69 | "petstore_auth":{ 70 | "type":"oauth2", 71 | "authorizationUrl":"https://petstore.swagger.io/oauth/authorize", 72 | "flow":"implicit", 73 | "scopes":{"read:pets":"read your pets","write:pets":"modify pets in your account"} 74 | } 75 | }, 76 | 77 | "definitions":{ 78 | "ApiResponse":{ 79 | "type":"object","properties":{"code":{"type":"integer","format":"int32"},"type":{"type":"string"},"message":{"type":"string"}} 80 | }, 81 | "Category":{ 82 | "type":"object","properties":{"id":{"type":"integer","format":"int64"},"name":{"type":"string"}},"xml":{"name":"Category"}} 83 | , 84 | "Pet":{ 85 | "type":"object","required":["name","photoUrls"],"properties":{"id":{"type":"integer","format":"int64"},"category":{"$ref":"#/definitions/Category"},"name":{"type":"string","example":"doggie"},"photoUrls":{"type":"array","xml":{"wrapped":true},"items":{"type":"string","xml":{"name":"photoUrl"}}},"tags":{"type":"array","xml":{"wrapped":true},"items":{"xml":{"name":"tag"},"$ref":"#/definitions/Tag"}},"status":{"type":"string","description":"pet status in the store","enum":["available","pending","sold"]}},"xml":{"name":"Pet"} 86 | }, 87 | "Tag":{ 88 | "type":"object","properties":{"id":{"type":"integer","format":"int64"},"name":{"type":"string"}},"xml":{"name":"Tag"} 89 | }, 90 | "Order":{ 91 | "type":"object","properties":{"id":{"type":"integer","format":"int64"},"petId":{"type":"integer","format":"int64"},"quantity":{"type":"integer","format":"int32"},"shipDate":{"type":"string","format":"date-time"},"status":{"type":"string","description":"Order Status","enum":["placed","approved","delivered"]},"complete":{"type":"boolean"}},"xml":{"name":"Order"} 92 | }, 93 | "User":{ 94 | "type":"object","properties":{"id":{"type":"integer","format":"int64"},"username":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"email":{"type":"string"},"password":{"type":"string"},"phone": {"type":"string"},"userStatus":{"type":"integer","format":"int32","description":"User Status"}},"xml":{"name":"User"} 95 | } 96 | }, 97 | 98 | "externalDocs":{ 99 | "description":"Find out more about Swagger", 100 | "url":"http://swagger.io" 101 | } 102 | } ``` -------------------------------------------------------------------------------- /src/mcp-server.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; 2 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 3 | import { z } from "zod"; 4 | import axios from "axios"; 5 | import SwaggerParser from "@apidevtools/swagger-parser"; 6 | import { OpenAPI } from "openapi-types"; 7 | import { Request, Response } from 'express'; 8 | import { AuthConfig, ToolInput, SecurityScheme } from './types.js'; 9 | 10 | let transport: SSEServerTransport | null = null; 11 | 12 | export class SwaggerMcpServer { 13 | private mcpServer: McpServer; 14 | private swaggerSpec: OpenAPI.Document | null = null; 15 | private apiBaseUrl: string; 16 | private defaultAuth: AuthConfig | undefined; 17 | private securitySchemes: Record<string, SecurityScheme> = {}; 18 | 19 | constructor(apiBaseUrl: string, defaultAuth?: AuthConfig) { 20 | console.debug('constructor', apiBaseUrl, defaultAuth); 21 | this.apiBaseUrl = apiBaseUrl; 22 | this.defaultAuth = defaultAuth; 23 | this.mcpServer = new McpServer({ 24 | name: "Swagger API MCP Server", 25 | version: "1.0.0", 26 | }); 27 | this.mcpServer.tool('test', 'test', { 28 | input: z.object({ 29 | test: z.string(), 30 | }), 31 | }, async ({ input }) => { 32 | return { content: [{ type: "text", text: "Hello, world!" }] }; 33 | }); 34 | } 35 | 36 | private getAuthHeaders(auth?: AuthConfig, operation?: OpenAPI.Operation): Record<string, string> { 37 | // Use provided auth or fall back to default auth 38 | const authConfig = auth || this.defaultAuth; 39 | if (!authConfig) return {}; 40 | 41 | // Check if operation requires specific security 42 | const requiredSchemes = operation?.security || (this.swaggerSpec as any)?.security || []; 43 | if (requiredSchemes.length === 0) return {}; 44 | 45 | switch (authConfig.type) { 46 | case 'basic': 47 | if (authConfig.username && authConfig.password) { 48 | const credentials = Buffer.from(`${authConfig.username}:${authConfig.password}`).toString('base64'); 49 | return { 'Authorization': `Basic ${credentials}` }; 50 | } 51 | break; 52 | case 'bearer': 53 | if (authConfig.token) { 54 | return { 'Authorization': `Bearer ${authConfig.token}` }; 55 | } 56 | break; 57 | case 'apiKey': 58 | // For Petstore, we know the API key goes in header named 'api_key' 59 | if (authConfig.apiKey) { 60 | return { 'api_key': authConfig.apiKey }; 61 | } 62 | break; 63 | case 'oauth2': 64 | if (authConfig.token) { 65 | return { 'Authorization': `Bearer ${authConfig.token}` }; 66 | } 67 | break; 68 | } 69 | return {}; 70 | } 71 | 72 | private getAuthQueryParams(auth?: AuthConfig): Record<string, string> { 73 | const authConfig = auth || this.defaultAuth; 74 | if (!authConfig) return {}; 75 | 76 | if (authConfig.type === 'apiKey' && authConfig.apiKey && authConfig.apiKeyName && authConfig.apiKeyIn === 'query') { 77 | return { [authConfig.apiKeyName]: authConfig.apiKey }; 78 | } 79 | 80 | return {}; 81 | } 82 | 83 | private extractSecuritySchemes() { 84 | if (!this.swaggerSpec) return; 85 | 86 | // OpenAPI 3.x 87 | const components = (this.swaggerSpec as any).components; 88 | if (components && components.securitySchemes) { 89 | this.securitySchemes = components.securitySchemes; 90 | return; 91 | } 92 | 93 | // Swagger 2.0 94 | const securityDefinitions = (this.swaggerSpec as any).securityDefinitions; 95 | if (securityDefinitions) { 96 | this.securitySchemes = securityDefinitions; 97 | } 98 | } 99 | 100 | private createAuthSchema(operation?: OpenAPI.Operation): z.ZodType<any> { 101 | const authTypes: string[] = ['none']; // Start with 'none' as default 102 | const authSchema: any = {}; 103 | 104 | // Check operation-specific security requirements 105 | const requiredSchemes = operation?.security || (this.swaggerSpec as any)?.security || []; 106 | const requiredSchemeNames = new Set( 107 | requiredSchemes.flatMap((scheme: any) => Object.keys(scheme)) 108 | ); 109 | 110 | for (const [key, scheme] of Object.entries(this.securitySchemes)) { 111 | const securityScheme = scheme as SecurityScheme; 112 | const isRequired = requiredSchemeNames.has(key); 113 | 114 | switch (securityScheme.type) { 115 | case 'basic': 116 | authTypes.push('basic'); 117 | if (isRequired || authTypes.length === 1) { 118 | authSchema.username = z.string(); 119 | authSchema.password = z.string(); 120 | } else { 121 | authSchema.username = z.string().optional(); 122 | authSchema.password = z.string().optional(); 123 | } 124 | break; 125 | case 'bearer': 126 | case 'http': 127 | if (securityScheme.scheme === 'bearer') { 128 | authTypes.push('bearer'); 129 | authSchema.token = isRequired ? z.string() : z.string().optional(); 130 | } 131 | break; 132 | case 'apiKey': 133 | authTypes.push('apiKey'); 134 | if (isRequired || authTypes.length === 1) { 135 | authSchema.apiKey = z.string(); 136 | if (securityScheme.in && securityScheme.name) { 137 | authSchema.apiKeyIn = z.enum(['header', 'query']).default(securityScheme.in as 'header' | 'query'); 138 | authSchema.apiKeyName = z.string().default(securityScheme.name); 139 | } 140 | } else { 141 | authSchema.apiKey = z.string().optional(); 142 | if (securityScheme.in && securityScheme.name) { 143 | authSchema.apiKeyIn = z.enum(['header', 'query']).optional().default(securityScheme.in as 'header' | 'query'); 144 | authSchema.apiKeyName = z.string().optional().default(securityScheme.name); 145 | } 146 | } 147 | break; 148 | case 'oauth2': 149 | authTypes.push('oauth2'); 150 | // Make token optional if API Key auth is available 151 | authSchema.token = isRequired && !authTypes.includes('apiKey') ? z.string() : z.string().optional(); 152 | break; 153 | } 154 | } 155 | 156 | // Add all auth types to the enum - ensure we have at least 'none' 157 | authSchema.type = z.enum(authTypes as [string, ...string[]]); 158 | 159 | const description = `Authentication configuration. Available methods: ${authTypes.join(', ')}. ` + 160 | Object.entries(this.securitySchemes) 161 | .map(([key, scheme]) => { 162 | const desc = (scheme as SecurityScheme).description || scheme.type; 163 | const required = requiredSchemeNames.has(key) ? ' (Required)' : ' (Optional)'; 164 | return `${key}: ${desc}${required}`; 165 | }) 166 | .join('. '); 167 | 168 | return z.object(authSchema).describe(description); 169 | } 170 | 171 | async loadSwaggerSpec(specUrlOrFile: string) { 172 | console.debug('Loading Swagger specification from:', specUrlOrFile); 173 | try { 174 | // Add auth headers for fetching the swagger spec if needed 175 | const headers = this.getAuthHeaders(); 176 | this.swaggerSpec = await SwaggerParser.parse(specUrlOrFile, { 177 | resolve: { http: { headers } } 178 | }) as OpenAPI.Document; 179 | 180 | const info = this.swaggerSpec.info; 181 | console.debug('Loaded Swagger spec:', { 182 | title: info.title, 183 | version: info.version, 184 | description: info.description?.substring(0, 100) + '...' 185 | }); 186 | 187 | // Extract security schemes 188 | this.extractSecuritySchemes(); 189 | console.debug('Security schemes found:', Object.keys(this.securitySchemes)); 190 | 191 | // Update server name with API info 192 | this.mcpServer = new McpServer({ 193 | name: info.title || "Swagger API Server", 194 | version: info.version || "1.0.0", 195 | description: info.description || undefined 196 | }); 197 | 198 | await this.registerTools(); 199 | } catch (error) { 200 | console.error("Failed to load Swagger specification:", error); 201 | throw error; 202 | } 203 | } 204 | 205 | private createZodSchema(parameter: OpenAPI.Parameter): z.ZodType<any> { 206 | const schema = (parameter as any).schema || parameter; 207 | 208 | switch (schema.type) { 209 | case 'string': 210 | return z.string().describe(schema.description || ''); 211 | case 'number': 212 | return z.number().describe(schema.description || ''); 213 | case 'integer': 214 | return z.number().int().describe(schema.description || ''); 215 | case 'boolean': 216 | return z.boolean().describe(schema.description || ''); 217 | case 'array': 218 | return z.array(this.createZodSchema(schema.items)).describe(schema.description || ''); 219 | case 'object': 220 | if (schema.properties) { 221 | const shape: { [key: string]: z.ZodType<any> } = {}; 222 | Object.entries(schema.properties).forEach(([key, prop]) => { 223 | shape[key] = this.createZodSchema(prop as OpenAPI.Parameter); 224 | }); 225 | return z.object(shape).describe(schema.description || ''); 226 | } 227 | return z.object({}).describe(schema.description || ''); 228 | default: 229 | return z.any().describe(schema.description || ''); 230 | } 231 | } 232 | 233 | private async registerTools() { 234 | console.debug('Starting tool registration process'); 235 | if (!this.swaggerSpec || !this.swaggerSpec.paths) { 236 | console.warn('No paths found in Swagger spec'); 237 | return; 238 | } 239 | 240 | const totalPaths = Object.keys(this.swaggerSpec.paths).length; 241 | console.debug(`Found ${totalPaths} paths to process`); 242 | 243 | for (const [path, pathItem] of Object.entries(this.swaggerSpec.paths)) { 244 | if (!pathItem) continue; 245 | for (const [method, operation] of Object.entries(pathItem)) { 246 | if (method === '$ref' || !operation) continue; 247 | 248 | const op = operation as OpenAPI.Operation; 249 | const operationId = op.operationId || `${method}-${path}`; 250 | console.log(`Register endpoint: ${method.toUpperCase()} ${path} (${operationId})`); 251 | 252 | // Create input schema based on parameters 253 | const inputShape: { [key: string]: z.ZodType<any> } = {}; 254 | const parameters = op.parameters || []; 255 | 256 | // Add auth parameters based on security schemes 257 | inputShape['auth'] = this.createAuthSchema(op); 258 | 259 | // Add API parameters 260 | parameters.forEach((param) => { 261 | if (param && 'name' in param && param.name) { 262 | inputShape[param.name] = this.createZodSchema(param); 263 | } 264 | }); 265 | 266 | console.debug(`Registering tool: ${operationId}`, { 267 | parameters: Object.keys(inputShape), 268 | hasAuth: !!inputShape['auth'] 269 | }); 270 | 271 | // Register the tool 272 | this.mcpServer.tool( 273 | operationId, 274 | `${op.summary || `${method.toUpperCase()} ${path}`}\n\n${op.description || ''}`, 275 | { 276 | input: z.object(inputShape), 277 | }, 278 | async ({ input }) => { 279 | console.debug(`Tool called: ${operationId}`, { 280 | params: Object.keys(input).filter(k => k !== 'auth'), 281 | hasAuth: !!input.auth 282 | }); 283 | try { 284 | const { auth, ...params } = input as ToolInput; 285 | console.debug('params', params); 286 | let url = this.apiBaseUrl + path; 287 | 288 | // Separate path parameters from query parameters 289 | const pathParams = new Set(); 290 | path.split('/').forEach(segment => { 291 | if (segment.startsWith('{') && segment.endsWith('}')) { 292 | pathParams.add(segment.slice(1, -1)); 293 | } 294 | }); 295 | 296 | // Replace path parameters 297 | Object.entries(params).forEach(([key, value]) => { 298 | if (pathParams.has(key)) { 299 | url = url.replace(`{${key}}`, encodeURIComponent(String(value))); 300 | } 301 | }); 302 | 303 | // Build query parameters object for GET requests 304 | const queryObject = method === 'get' ? 305 | Object.entries(params) 306 | .filter(([key]) => !pathParams.has(key)) 307 | .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}) 308 | : {}; 309 | 310 | const headers = this.getAuthHeaders(auth, op); 311 | const queryParams = this.getAuthQueryParams(auth); 312 | 313 | console.debug('url', url); 314 | console.debug('method', method); 315 | console.debug('headers', headers); 316 | console.debug('params', params); 317 | console.debug('queryParams', queryParams); 318 | 319 | const response = await axios({ 320 | method: method as string, 321 | url: url, 322 | headers, 323 | data: method !== 'get' ? params : undefined, 324 | params: { ...queryObject, ...queryParams }, 325 | paramsSerializer: (params) => { 326 | const searchParams = new URLSearchParams(); 327 | Object.entries(params).forEach(([key, value]) => { 328 | if (Array.isArray(value)) { 329 | // Handle arrays by adding multiple entries with the same key 330 | value.forEach(v => searchParams.append(key, v)); 331 | } else { 332 | searchParams.append(key, value as string); 333 | } 334 | }); 335 | return searchParams.toString(); 336 | } 337 | }); 338 | console.debug('response.headers', response.headers); 339 | console.debug('response.data', response.data); 340 | 341 | return { 342 | content: [ 343 | { type: "text", text: JSON.stringify(response.data, null, 2) }, 344 | // http status code 345 | { type: "text", text: `HTTP Status Code: ${response.status}` }, 346 | // // http headers 347 | // { type: "text", text: JSON.stringify(response.headers, null, 2) }, 348 | ], 349 | }; 350 | } catch (error) { 351 | console.error(`Error in ${operationId}:`, error); 352 | if (axios.isAxiosError(error) && error.response) { 353 | return { 354 | content: [{ 355 | type: "text", text: `Error ${error.response.status}: ${JSON.stringify(error.response.data, null, 2)}` 356 | }], 357 | }; 358 | } 359 | return { 360 | content: [{ type: "text", text: `Error: ${error}` }], 361 | }; 362 | } 363 | } 364 | ); 365 | } 366 | } 367 | } 368 | 369 | getServer() { 370 | return this.mcpServer; 371 | } 372 | 373 | handleSSE(res: Response) { 374 | console.debug('MCP handleSSE'); 375 | transport = new SSEServerTransport("/messages", res); 376 | this.mcpServer.connect(transport); 377 | } 378 | 379 | handleMessage(req: Request, res: Response) { 380 | console.debug('MCP handleMessage', req.body); 381 | if (transport) { 382 | try { 383 | transport.handlePostMessage(req, res); 384 | } catch (error) { 385 | console.error('Error handling message:', error); 386 | } 387 | } else { 388 | console.warn('no transport'); 389 | } 390 | } 391 | } 392 | ```