# 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: -------------------------------------------------------------------------------- ``` # Server Configuration PORT=3000 # API Authentication API_USERNAME= API_PASSWORD= API_TOKEN= # Default API Configuration DEFAULT_API_BASE_URL= DEFAULT_SWAGGER_URL= ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` # Dependencies node_modules/ yarn-debug.log* yarn-error.log* # Build output dist/ build/ # Environment variables .env .env.local .env.*.local # IDE and editor files .idea/ .vscode/ *.swp *.swo .DS_Store # Test coverage coverage/ # Logs logs/ *.log npm-debug.log* ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown # Swagger MCP Server A server that ingests and serves Swagger/OpenAPI specifications through the Model Context Protocol (MCP). ## Features - Loads Swagger/OpenAPI specifications - Supports multiple authentication methods: - Basic Auth - Bearer Token - API Key (header or query) - OAuth2 - Automatically generates MCP tools from API endpoints - Server-Sent Events (SSE) support for real-time communication - TypeScript support ## Security This is a personal server!! Do not expose it to the public internet. If the underlying API requires authentication, you should not expose the MCP server to the public internet. ## TODO - secrets - the MCP server should be able to use secrets from the user to authenticate requests to the API - Comprehensive test suite ## Prerequisites - Node.js (v18 or higher) - Yarn package manager - TypeScript ## Installation 1. Clone the repository: ```bash git clone https://github.com/dcolley/swagger-mcp.git cd swagger-mcp ``` 2. Install dependencies: ```bash yarn install ``` 3. Create a `.env` file based on the example: ```bash cp .env.example .env ``` 4. Configure your Swagger/OpenAPI specification: - Place your Swagger file in the project (e.g., `swagger.json`) - Or provide a URL to your Swagger specification 5. Update the configuration in `config.json` with your server settings: ```json { "server": { "host": "localhost", "port": 3000 }, "swagger": { "url": "url-or-path/to/your/swagger.json", "apiBaseUrl": "https://api.example.com", // Fallback if not specified in Swagger "defaultAuth": { // Fallback if not specified in Swagger "type": "apiKey", "apiKey": "your-api-key", "apiKeyName": "api_key", "apiKeyIn": "header" } } } ``` Note: The server prioritizes settings from the Swagger specification over the config file: - If the Swagger file contains a `servers` array, the first server URL will be used as the base URL - If the Swagger file defines security schemes, they will be used for authentication - The config file settings serve as fallbacks when not specified in the Swagger file ## Usage 1. Start the development server: ```bash yarn dev ``` 2. Build for production: ```bash yarn build ``` 3. Start the production server: ```bash yarn start ``` ## API Endpoints - `GET /health` - Check server health status - `GET /sse` - Establish Server-Sent Events connection - `POST /messages` - Send messages to the MCP server ## Testing Run the test suite: ```bash # Run tests once yarn test # Run tests in watch mode yarn test:watch # Run tests with coverage report yarn test:coverage ``` ## Authentication The server supports various authentication methods. Configure them in the `config.json` file as fallbacks when not specified in the Swagger file: ### Basic Auth ```json { "defaultAuth": { "type": "basic", "username": "your-username", "password": "your-password" } } ``` ### Bearer Token ```json { "defaultAuth": { "type": "bearer", "token": "your-bearer-token" } } ``` ### API Key ```json { "defaultAuth": { "type": "apiKey", "apiKey": "your-api-key", "apiKeyName": "X-API-Key", "apiKeyIn": "header" } } ``` ### OAuth2 ```json { "defaultAuth": { "type": "oauth2", "token": "your-oauth-token" } } ``` ## Development 1. Start the development server: ```bash yarn dev ``` <!-- 2. Make changes to the code 3. Run tests to ensure everything works: ```bash yarn test ``` 4. Build the project: ```bash yarn build ``` --> <!-- ## Contributing 1. Fork the repository 2. Create your feature branch (`git checkout -b feature/amazing-feature`) 3. Commit your changes (`git commit -m 'Add some amazing feature'`) 4. Push to the branch (`git push origin feature/amazing-feature`) 5. Open a Pull Request --> ## License This project is licensed under the Apache 2.0 License. ## Environment Variables - `PORT`: Server port (default: 3000) - `API_USERNAME`: Username for API authentication (fallback) - `API_PASSWORD`: Password for API authentication (fallback) - `API_TOKEN`: API token for authentication (fallback) - `DEFAULT_API_BASE_URL`: Default base URL for API endpoints (fallback) - `DEFAULT_SWAGGER_URL`: Default Swagger specification URL ``` -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- ```javascript module.exports = { preset: 'ts-jest', testEnvironment: 'node', roots: ['<rootDir>/tests'], transform: { '^.+\\.tsx?$': 'ts-jest', }, testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$', moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], }; ``` -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- ```json { "swagger": { "url": "https://petstore.swagger.io/v2/swagger.json", "apiBaseUrl": "https://petstore.swagger.io/v2", "defaultAuth": { "type": "apiKey", "apiKey": "special-key", "apiKeyName": "api_key", "apiKeyIn": "header" } }, "log": { "level": "info" }, "server": { "port": 3000, "host": "0.0.0.0" } } ``` -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- ```typescript export interface SwaggerConfig { swaggerUrl?: string; swaggerFile?: string; apiBaseUrl: string; auth?: AuthConfig; } export interface AuthConfig { type: 'basic' | 'bearer' | 'apiKey' | 'oauth2'; username?: string; password?: string; token?: string; apiKey?: string; apiKeyName?: string; apiKeyIn?: 'header' | 'query'; } export interface ToolInput { auth?: AuthConfig; [key: string]: any; } export interface SecurityScheme { type: string; description?: string; name?: string; in?: string; scheme?: string; flows?: { implicit?: { authorizationUrl: string; scopes: Record<string, string>; }; [key: string]: any; }; } ``` -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- ```typescript export interface SwaggerConfig { swaggerFile?: string; swaggerUrl?: string; apiBaseUrl: string; auth?: AuthConfig; } export interface ServerConfig { port: number; username?: string; password?: string; token?: string; } export interface AuthConfig { type: 'basic' | 'bearer' | 'apiKey' | 'oauth2'; username?: string; password?: string; token?: string; apiKey?: string; apiKeyIn?: 'header' | 'query'; apiKeyName?: string; } export interface SecurityScheme { type: string; description?: string; name?: string; in?: string; scheme?: string; bearerFormat?: string; flows?: any; } export interface ToolInput { auth?: AuthConfig; [key: string]: any; } ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json { "name": "swagger-mcp", "version": "1.0.0", "description": "Server that ingests and serves Swagger/OpenAPI specifications", "main": "dist/server.js", "scripts": { "start": "node dist/server.js", "dev": "NODE_OPTIONS='--loader ts-node/esm' ts-node src/server.ts", "build": "tsc", "watch": "tsc -w", "test": "jest", "test:watch": "jest --watch", "test:coverage": "jest --coverage" }, "keywords": [ "swagger", "openapi", "api", "documentation" ], "author": "", "license": "ISC", "dependencies": { "@apidevtools/swagger-parser": "^10.1.0", "@modelcontextprotocol/sdk": "^1.7.0", "@types/cors": "^2.8.17", "@types/express": "^5.0.0", "@types/swagger-parser": "^7.0.1", "axios": "^1.8.3", "cors": "^2.8.5", "dotenv": "^16.4.5", "eventsource": "^3.0.5", "express": "^4.18.3", "node-fetch": "^3.3.2", "openapi-types": "^12.1.3", "ts-node": "^10.9.2", "tslib": "^2.8.1", "typescript": "^5.4.2", "zod": "^3.22.4" }, "devDependencies": { "@types/jest": "^29.5.14", "@types/supertest": "^6.0.2", "jest": "^29.7.0", "supertest": "^7.0.0", "ts-jest": "^29.2.6" }, "packageManager": "[email protected]+sha1.1959a18351b811cdeedbd484a8f86c3cc3bbaf72" } ``` -------------------------------------------------------------------------------- /tests/test-pets.js: -------------------------------------------------------------------------------- ```javascript import fetch from 'node-fetch'; import { EventSource } from 'eventsource'; async function findPetsBySold() { try { // First check if server is ready const healthResponse = await fetch('http://localhost:3000/health'); const health = await healthResponse.json(); if (health.mcpServer !== 'initialized') { console.error('Server is not ready. Please wait for it to initialize.'); return; } // Create SSE connection const eventSource = new EventSource('http://localhost:3000/sse'); eventSource.onopen = async () => { console.log('SSE connection opened'); // Send the request try { const response = await fetch('http://localhost:3000/messages', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ type: 'invoke', tool: 'findPetsByStatus', input: { status: ['sold'] } }) }); const data = await response.json(); console.log('Response:', data); eventSource.close(); } catch (error) { console.error('Error sending message:', error.message); eventSource.close(); } }; eventSource.onerror = (error) => { console.error('SSE error:', error); eventSource.close(); }; eventSource.onmessage = (event) => { console.log('Received:', event.data); }; } catch (error) { console.error('Error:', error.message); if (error.message.includes('ECONNREFUSED')) { console.log('Make sure the server is running on port 3000'); } } } // Run the test findPetsBySold(); ``` -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- ```typescript import { z } from 'zod'; import fs from 'fs/promises'; import path from 'path'; // Define the configuration schema export const ConfigSchema = z.object({ swagger: z.object({ url: z.string().url(), apiBaseUrl: z.string().url(), defaultAuth: z.object({ type: z.enum(['basic', 'bearer', 'apiKey', 'oauth2']), token: z.string().optional(), username: z.string().optional(), password: z.string().optional(), apiKey: z.string().optional(), apiKeyName: z.string().optional(), apiKeyIn: z.enum(['header', 'query']).optional(), }).optional(), }), log: z.object({ level: z.enum(['debug', 'info', 'warn', 'error']), }), server: z.object({ port: z.number().default(3000), host: z.string().default('0.0.0.0'), }), }); export type Config = z.infer<typeof ConfigSchema>; const defaultConfig: Config = { swagger: { url: 'https://petstore.swagger.io/v2/swagger.json', apiBaseUrl: 'https://petstore.swagger.io/v2', defaultAuth: { type: 'apiKey', apiKey: 'special-key', apiKeyName: 'api_key', apiKeyIn: 'header', }, }, log: { level: 'info', }, server: { port: 3000, host: '0.0.0.0', }, }; export async function loadConfig(configPath?: string): Promise<Config> { try { // If no config path provided, create default config file if (!configPath) { configPath = path.join(process.cwd(), 'config.json'); // Check if config file exists, if not create it with default values try { await fs.access(configPath); } catch { await fs.writeFile(configPath, JSON.stringify(defaultConfig, null, 2)); console.log(`Created default configuration file at ${configPath}`); } } const configFile = await fs.readFile(configPath, 'utf-8'); const config = JSON.parse(configFile); return ConfigSchema.parse(config); } catch (error) { if (error instanceof z.ZodError) { console.error('Invalid configuration:', error.errors); } else { console.error('Error loading configuration:', error); } console.log('Using default configuration'); return defaultConfig; } } ``` -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- ```typescript import express, { Request, Response, Router } from 'express'; import cors from 'cors'; import dotenv from 'dotenv'; import { SwaggerMcpServer } from './mcp-server'; import { loadConfig } from './config'; // Load environment variables dotenv.config(); const app = express(); const router = Router(); let mcpServer: SwaggerMcpServer | null = null; // Middleware // app.use(cors()); // app.use(express.json()); // Routes const handleSSE = async (req: Request, res: Response) => { console.debug('SSE connection request received'); if (!mcpServer) { console.warn('MCP server not initialized - rejecting SSE connection'); res.status(400).json({ error: 'MCP server not initialized' }); return; } console.debug('Establishing SSE connection...'); mcpServer.handleSSE(res); }; const handleMessage = async (req: Request, res: Response) => { console.debug('Message received:', { method: req.method, path: req.path, body: req.body }); if (!mcpServer) { console.warn('MCP server not initialized - rejecting message'); res.status(400).json({ error: 'MCP server not initialized' }); return; } mcpServer.handleMessage(req, res); }; const handleHealth = (_req: Request, res: Response) => { console.debug('Health check request received'); res.json({ status: 'ok', mcpServer: mcpServer ? 'initialized' : 'not initialized' }); }; // // Register routes // router.get('/sse', handleSSE); // router.post('/messages', handleMessage); // router.get('/health', handleHealth); // Mount router // app.use('/', router); app.get('/sse', handleSSE); app.post('/messages', handleMessage); app.get('/health', handleHealth); // Initialize server async function initializeServer() { try { console.log('Starting server initialization...'); // Load configuration const config = await loadConfig(); // set app logging level process.env.LOG_LEVEL = config.log?.level || 'info'; console.debug('Configuration loaded:', { swaggerUrl: config.swagger.url, apiBaseUrl: config.swagger.apiBaseUrl, hasDefaultAuth: !!config.swagger.defaultAuth }); // Create and initialize MCP server console.log('Creating MCP server instance...'); mcpServer = new SwaggerMcpServer(config.swagger.apiBaseUrl, config.swagger.defaultAuth); console.log('Loading Swagger specification...'); await mcpServer.loadSwaggerSpec(config.swagger.url); console.debug('Swagger specification loaded successfully'); // Start the server app.listen(config.server.port, config.server.host, () => { console.log('Server initialization complete'); console.log(`Server is running on http://${config.server.host}:${config.server.port}`); console.log('Swagger specification loaded from:', config.swagger.url); console.log('API Base URL:', config.swagger.apiBaseUrl); }); } catch (error) { console.error('Failed to initialize server:', error); process.exit(1); } } // Start the server initializeServer(); ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json { "compilerOptions": { /* Visit https://aka.ms/tsconfig to read more about this file */ /* Projects */ // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ /* Language and Environment */ "target": "ES2020", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ // "jsx": "preserve", /* Specify what JSX code is generated. */ // "libReplacement": true, /* Enable lib replacement. */ // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ /* Modules */ "module": "NodeNext", /* Specify what module code is generated. */ "moduleResolution": "NodeNext", /* Specify how TypeScript looks up a file from a given module specifier. */ // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ // "types": [], /* Specify type package names to be included without being referenced in a source file. */ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ // "rewriteRelativeImportExtensions": true, /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */ // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ // "noUncheckedSideEffectImports": true, /* Check side effect imports. */ // "resolveJsonModule": true, /* Enable importing .json files. */ // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ // "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */ /* JavaScript Support */ // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ /* Emit */ "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ "declarationMap": true, /* Create sourcemaps for d.ts files. */ "emitDeclarationOnly": false, /* Only output d.ts files and not JavaScript files. */ "sourceMap": true, /* Create source map files for emitted JavaScript files. */ "noEmit": false, /* Disable emitting files from a compilation. */ "outDir": "dist", /* Specify an output folder for all emitted files. */ "removeComments": true, /* Disable emitting comments. */ "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ "newLine": "crlf", /* Set the newline character for emitting files. */ "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ /* Interop Constraints */ // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ // "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. */ // "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */ // "erasableSyntaxOnly": true, /* Do not allow runtime constructs that are not part of ECMAScript. */ // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ /* Type Checking */ "strict": true, /* Enable all strict type-checking options. */ // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ // "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */ // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ /* Completeness */ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ "skipLibCheck": true /* Skip type checking all .d.ts files. */ }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"] } ``` -------------------------------------------------------------------------------- /examples/swagger-pet-store.json: -------------------------------------------------------------------------------- ```json { "swagger":"2.0", "info":{ "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"} }, "host":"petstore.swagger.io", "basePath":"/v2", "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", "externalDocs":{"description":"Find out more about our store","url":"http://swagger.io"}}], "schemes":["https","http"], "paths":{ "/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"]}]}}, "/pet":{ "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"]}]} }, "/pet/findByStatus":{ "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"], "parameters":[ {"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"} ], "responses":{ "200":{"description":"successful operation","schema":{"type":"array","items":{"$ref":"#/definitions/Pet"}}}, "400":{"description":"Invalid status value"} }, "security":[{"petstore_auth":["write:pets","read:pets"]} ] } }, "/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}}, "/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"]}]}}, "/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":[]}]}}, "/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"}}}}, "/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"}}}}, "/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"}}}}, "/user/{username}":{ "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"}}}, "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"}}}, "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"}}}}, "/user/login":{ "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"}}} }, "/user/logout":{ "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"}}} }, "/user/createWithArray":{ "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"}}} }, "/user":{ "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"}}} } }, "securityDefinitions":{ "api_key":{"type":"apiKey","name":"api_key","in":"header"}, "petstore_auth":{ "type":"oauth2", "authorizationUrl":"https://petstore.swagger.io/oauth/authorize", "flow":"implicit", "scopes":{"read:pets":"read your pets","write:pets":"modify pets in your account"} } }, "definitions":{ "ApiResponse":{ "type":"object","properties":{"code":{"type":"integer","format":"int32"},"type":{"type":"string"},"message":{"type":"string"}} }, "Category":{ "type":"object","properties":{"id":{"type":"integer","format":"int64"},"name":{"type":"string"}},"xml":{"name":"Category"}} , "Pet":{ "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"} }, "Tag":{ "type":"object","properties":{"id":{"type":"integer","format":"int64"},"name":{"type":"string"}},"xml":{"name":"Tag"} }, "Order":{ "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"} }, "User":{ "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"} } }, "externalDocs":{ "description":"Find out more about Swagger", "url":"http://swagger.io" } } ``` -------------------------------------------------------------------------------- /src/mcp-server.ts: -------------------------------------------------------------------------------- ```typescript import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; import axios from "axios"; import SwaggerParser from "@apidevtools/swagger-parser"; import { OpenAPI } from "openapi-types"; import { Request, Response } from 'express'; import { AuthConfig, ToolInput, SecurityScheme } from './types.js'; let transport: SSEServerTransport | null = null; export class SwaggerMcpServer { private mcpServer: McpServer; private swaggerSpec: OpenAPI.Document | null = null; private apiBaseUrl: string; private defaultAuth: AuthConfig | undefined; private securitySchemes: Record<string, SecurityScheme> = {}; constructor(apiBaseUrl: string, defaultAuth?: AuthConfig) { console.debug('constructor', apiBaseUrl, defaultAuth); this.apiBaseUrl = apiBaseUrl; this.defaultAuth = defaultAuth; this.mcpServer = new McpServer({ name: "Swagger API MCP Server", version: "1.0.0", }); this.mcpServer.tool('test', 'test', { input: z.object({ test: z.string(), }), }, async ({ input }) => { return { content: [{ type: "text", text: "Hello, world!" }] }; }); } private getAuthHeaders(auth?: AuthConfig, operation?: OpenAPI.Operation): Record<string, string> { // Use provided auth or fall back to default auth const authConfig = auth || this.defaultAuth; if (!authConfig) return {}; // Check if operation requires specific security const requiredSchemes = operation?.security || (this.swaggerSpec as any)?.security || []; if (requiredSchemes.length === 0) return {}; switch (authConfig.type) { case 'basic': if (authConfig.username && authConfig.password) { const credentials = Buffer.from(`${authConfig.username}:${authConfig.password}`).toString('base64'); return { 'Authorization': `Basic ${credentials}` }; } break; case 'bearer': if (authConfig.token) { return { 'Authorization': `Bearer ${authConfig.token}` }; } break; case 'apiKey': // For Petstore, we know the API key goes in header named 'api_key' if (authConfig.apiKey) { return { 'api_key': authConfig.apiKey }; } break; case 'oauth2': if (authConfig.token) { return { 'Authorization': `Bearer ${authConfig.token}` }; } break; } return {}; } private getAuthQueryParams(auth?: AuthConfig): Record<string, string> { const authConfig = auth || this.defaultAuth; if (!authConfig) return {}; if (authConfig.type === 'apiKey' && authConfig.apiKey && authConfig.apiKeyName && authConfig.apiKeyIn === 'query') { return { [authConfig.apiKeyName]: authConfig.apiKey }; } return {}; } private extractSecuritySchemes() { if (!this.swaggerSpec) return; // OpenAPI 3.x const components = (this.swaggerSpec as any).components; if (components && components.securitySchemes) { this.securitySchemes = components.securitySchemes; return; } // Swagger 2.0 const securityDefinitions = (this.swaggerSpec as any).securityDefinitions; if (securityDefinitions) { this.securitySchemes = securityDefinitions; } } private createAuthSchema(operation?: OpenAPI.Operation): z.ZodType<any> { const authTypes: string[] = ['none']; // Start with 'none' as default const authSchema: any = {}; // Check operation-specific security requirements const requiredSchemes = operation?.security || (this.swaggerSpec as any)?.security || []; const requiredSchemeNames = new Set( requiredSchemes.flatMap((scheme: any) => Object.keys(scheme)) ); for (const [key, scheme] of Object.entries(this.securitySchemes)) { const securityScheme = scheme as SecurityScheme; const isRequired = requiredSchemeNames.has(key); switch (securityScheme.type) { case 'basic': authTypes.push('basic'); if (isRequired || authTypes.length === 1) { authSchema.username = z.string(); authSchema.password = z.string(); } else { authSchema.username = z.string().optional(); authSchema.password = z.string().optional(); } break; case 'bearer': case 'http': if (securityScheme.scheme === 'bearer') { authTypes.push('bearer'); authSchema.token = isRequired ? z.string() : z.string().optional(); } break; case 'apiKey': authTypes.push('apiKey'); if (isRequired || authTypes.length === 1) { authSchema.apiKey = z.string(); if (securityScheme.in && securityScheme.name) { authSchema.apiKeyIn = z.enum(['header', 'query']).default(securityScheme.in as 'header' | 'query'); authSchema.apiKeyName = z.string().default(securityScheme.name); } } else { authSchema.apiKey = z.string().optional(); if (securityScheme.in && securityScheme.name) { authSchema.apiKeyIn = z.enum(['header', 'query']).optional().default(securityScheme.in as 'header' | 'query'); authSchema.apiKeyName = z.string().optional().default(securityScheme.name); } } break; case 'oauth2': authTypes.push('oauth2'); // Make token optional if API Key auth is available authSchema.token = isRequired && !authTypes.includes('apiKey') ? z.string() : z.string().optional(); break; } } // Add all auth types to the enum - ensure we have at least 'none' authSchema.type = z.enum(authTypes as [string, ...string[]]); const description = `Authentication configuration. Available methods: ${authTypes.join(', ')}. ` + Object.entries(this.securitySchemes) .map(([key, scheme]) => { const desc = (scheme as SecurityScheme).description || scheme.type; const required = requiredSchemeNames.has(key) ? ' (Required)' : ' (Optional)'; return `${key}: ${desc}${required}`; }) .join('. '); return z.object(authSchema).describe(description); } async loadSwaggerSpec(specUrlOrFile: string) { console.debug('Loading Swagger specification from:', specUrlOrFile); try { // Add auth headers for fetching the swagger spec if needed const headers = this.getAuthHeaders(); this.swaggerSpec = await SwaggerParser.parse(specUrlOrFile, { resolve: { http: { headers } } }) as OpenAPI.Document; const info = this.swaggerSpec.info; console.debug('Loaded Swagger spec:', { title: info.title, version: info.version, description: info.description?.substring(0, 100) + '...' }); // Extract security schemes this.extractSecuritySchemes(); console.debug('Security schemes found:', Object.keys(this.securitySchemes)); // Update server name with API info this.mcpServer = new McpServer({ name: info.title || "Swagger API Server", version: info.version || "1.0.0", description: info.description || undefined }); await this.registerTools(); } catch (error) { console.error("Failed to load Swagger specification:", error); throw error; } } private createZodSchema(parameter: OpenAPI.Parameter): z.ZodType<any> { const schema = (parameter as any).schema || parameter; switch (schema.type) { case 'string': return z.string().describe(schema.description || ''); case 'number': return z.number().describe(schema.description || ''); case 'integer': return z.number().int().describe(schema.description || ''); case 'boolean': return z.boolean().describe(schema.description || ''); case 'array': return z.array(this.createZodSchema(schema.items)).describe(schema.description || ''); case 'object': if (schema.properties) { const shape: { [key: string]: z.ZodType<any> } = {}; Object.entries(schema.properties).forEach(([key, prop]) => { shape[key] = this.createZodSchema(prop as OpenAPI.Parameter); }); return z.object(shape).describe(schema.description || ''); } return z.object({}).describe(schema.description || ''); default: return z.any().describe(schema.description || ''); } } private async registerTools() { console.debug('Starting tool registration process'); if (!this.swaggerSpec || !this.swaggerSpec.paths) { console.warn('No paths found in Swagger spec'); return; } const totalPaths = Object.keys(this.swaggerSpec.paths).length; console.debug(`Found ${totalPaths} paths to process`); for (const [path, pathItem] of Object.entries(this.swaggerSpec.paths)) { if (!pathItem) continue; for (const [method, operation] of Object.entries(pathItem)) { if (method === '$ref' || !operation) continue; const op = operation as OpenAPI.Operation; const operationId = op.operationId || `${method}-${path}`; console.log(`Register endpoint: ${method.toUpperCase()} ${path} (${operationId})`); // Create input schema based on parameters const inputShape: { [key: string]: z.ZodType<any> } = {}; const parameters = op.parameters || []; // Add auth parameters based on security schemes inputShape['auth'] = this.createAuthSchema(op); // Add API parameters parameters.forEach((param) => { if (param && 'name' in param && param.name) { inputShape[param.name] = this.createZodSchema(param); } }); console.debug(`Registering tool: ${operationId}`, { parameters: Object.keys(inputShape), hasAuth: !!inputShape['auth'] }); // Register the tool this.mcpServer.tool( operationId, `${op.summary || `${method.toUpperCase()} ${path}`}\n\n${op.description || ''}`, { input: z.object(inputShape), }, async ({ input }) => { console.debug(`Tool called: ${operationId}`, { params: Object.keys(input).filter(k => k !== 'auth'), hasAuth: !!input.auth }); try { const { auth, ...params } = input as ToolInput; console.debug('params', params); let url = this.apiBaseUrl + path; // Separate path parameters from query parameters const pathParams = new Set(); path.split('/').forEach(segment => { if (segment.startsWith('{') && segment.endsWith('}')) { pathParams.add(segment.slice(1, -1)); } }); // Replace path parameters Object.entries(params).forEach(([key, value]) => { if (pathParams.has(key)) { url = url.replace(`{${key}}`, encodeURIComponent(String(value))); } }); // Build query parameters object for GET requests const queryObject = method === 'get' ? Object.entries(params) .filter(([key]) => !pathParams.has(key)) .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}) : {}; const headers = this.getAuthHeaders(auth, op); const queryParams = this.getAuthQueryParams(auth); console.debug('url', url); console.debug('method', method); console.debug('headers', headers); console.debug('params', params); console.debug('queryParams', queryParams); const response = await axios({ method: method as string, url: url, headers, data: method !== 'get' ? params : undefined, params: { ...queryObject, ...queryParams }, paramsSerializer: (params) => { const searchParams = new URLSearchParams(); Object.entries(params).forEach(([key, value]) => { if (Array.isArray(value)) { // Handle arrays by adding multiple entries with the same key value.forEach(v => searchParams.append(key, v)); } else { searchParams.append(key, value as string); } }); return searchParams.toString(); } }); console.debug('response.headers', response.headers); console.debug('response.data', response.data); return { content: [ { type: "text", text: JSON.stringify(response.data, null, 2) }, // http status code { type: "text", text: `HTTP Status Code: ${response.status}` }, // // http headers // { type: "text", text: JSON.stringify(response.headers, null, 2) }, ], }; } catch (error) { console.error(`Error in ${operationId}:`, error); if (axios.isAxiosError(error) && error.response) { return { content: [{ type: "text", text: `Error ${error.response.status}: ${JSON.stringify(error.response.data, null, 2)}` }], }; } return { content: [{ type: "text", text: `Error: ${error}` }], }; } } ); } } } getServer() { return this.mcpServer; } handleSSE(res: Response) { console.debug('MCP handleSSE'); transport = new SSEServerTransport("/messages", res); this.mcpServer.connect(transport); } handleMessage(req: Request, res: Response) { console.debug('MCP handleMessage', req.body); if (transport) { try { transport.handlePostMessage(req, res); } catch (error) { console.error('Error handling message:', error); } } else { console.warn('no transport'); } } } ```