# Directory Structure ``` ├── .gitignore ├── package-lock.json ├── package.json ├── readme.md ├── src │ └── index.ts └── tsconfig.json ``` # Files -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | /node_modules 2 | /build ``` -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- ```markdown 1 | # Swagger/OpenAPI MCP Server 2 | 3 | A Model Context Protocol (MCP) server that allows LLMs to explore and interact with Swagger/OpenAPI specifications. This server provides tools and resources for loading API specifications, browsing endpoints, and getting detailed information about API operations. 4 | 5 | ## Installation 6 | 7 | 1. Clone or create the project directory 8 | 2. Install dependencies: 9 | 10 | ```bash 11 | npm install 12 | ``` 13 | 14 | 3. Build the TypeScript code: 15 | 16 | ```bash 17 | npm run build 18 | ``` 19 | 20 | ## Usage 21 | 22 | 23 | ### Available Tools 24 | 25 | #### `load_api` 26 | Load an OpenAPI/Swagger specification into the server. 27 | 28 | **Parameters:** 29 | - `apiId` (string): Unique identifier for this API 30 | - `source` (string): URL or file path to the OpenAPI/Swagger specification 31 | 32 | **Example:** 33 | ```json 34 | { 35 | "name": "load_api", 36 | "arguments": { 37 | "apiId": "petstore", 38 | "source": "https://petstore.swagger.io/v2/swagger.json" 39 | } 40 | } 41 | ``` 42 | 43 | #### `get_endpoint_details` 44 | Get detailed information about a specific API endpoint. 45 | 46 | **Parameters:** 47 | - `apiId` (string): ID of the loaded API 48 | - `method` (string): HTTP method (GET, POST, etc.) 49 | - `path` (string): API endpoint path 50 | - `natural` (boolean, optional): If true, returns a human-readable summary 51 | 52 | **Example:** 53 | ```json 54 | { 55 | "name": "get_endpoint_details", 56 | "arguments": { 57 | "apiId": "petstore", 58 | "method": "GET", 59 | "path": "/pet/{petId}", 60 | "natural": true 61 | } 62 | } 63 | ``` 64 | 65 | #### `list_apis` 66 | List all currently loaded API specifications. 67 | 68 | **Parameters:** None 69 | 70 | #### `search_endpoints` 71 | Search for endpoints matching a specific pattern. 72 | 73 | **Parameters:** 74 | - `apiId` (string): ID of the loaded API 75 | - `pattern` (string): Search pattern for endpoint paths or descriptions 76 | 77 | **Example:** 78 | ```json 79 | { 80 | "name": "search_endpoints", 81 | "arguments": { 82 | "apiId": "petstore", 83 | "pattern": "pet" 84 | } 85 | } 86 | ``` 87 | 88 | ### Available Resources 89 | 90 | #### `swagger://{apiId}/load` 91 | Get overview information about a loaded API specification. 92 | 93 | #### `swagger://{apiId}/endpoints` 94 | Get a list of all available endpoints for an API. 95 | 96 | #### `swagger://{apiId}/endpoint/{method}/{path}` 97 | Get detailed information about a specific endpoint. 98 | 99 | ## Configuration with Claude Desktop 100 | 101 | To use this server with Claude Desktop, add the following to your `claude_desktop_config.json`: 102 | 103 | ```json 104 | { 105 | "mcpServers": { 106 | "swagger-explorer": { 107 | "command": "node", 108 | "args": ["/path/to/your/swagger-mcp-server/build/index.js"] 109 | } 110 | } 111 | } 112 | ``` 113 | 114 | Replace `/path/to/your/swagger-mcp-server` with the actual path to your project directory. 115 | 116 | 117 | ## License 118 | 119 | MIT License 120 | ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "Node16", 5 | "moduleResolution": "Node16", 6 | "outDir": "./build", 7 | "rootDir": "./src", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "allowSyntheticDefaultImports": true, 13 | "resolveJsonModule": true, 14 | "declaration": true, 15 | "declarationMap": true, 16 | "sourceMap": true 17 | }, 18 | "include": [ 19 | "src/**/*" 20 | ], 21 | "exclude": [ 22 | "node_modules", 23 | "build" 24 | ] 25 | } ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "swagger-mcp-server", 3 | "version": "1.0.0", 4 | "description": "MCP Server for exploring Swagger/OpenAPI specifications", 5 | "main": "build/index.js", 6 | "bin": { 7 | "swagger-mcp-server": "./build/index.js" 8 | }, 9 | "scripts": { 10 | "build": "tsc && chmod 755 build/index.js", 11 | "dev": "tsc && node build/index.js", 12 | "start": "node build/index.js", 13 | "watch": "tsc --watch" 14 | }, 15 | "dependencies": { 16 | "@modelcontextprotocol/sdk": "^1.0.0", 17 | "@apidevtools/swagger-parser": "^10.1.0", 18 | "openapi-types": "^12.1.3", 19 | "zod": "^3.23.8" 20 | }, 21 | "devDependencies": { 22 | "@types/node": "^20.10.0", 23 | "typescript": "^5.3.0" 24 | }, 25 | "keywords": [ 26 | "mcp", 27 | "model-context-protocol", 28 | "swagger", 29 | "openapi", 30 | "api", 31 | "documentation" 32 | ], 33 | "author": "Your Name", 34 | "license": "MIT", 35 | "files": [ 36 | "build/" 37 | ], 38 | "engines": { 39 | "node": ">=18.0.0" 40 | } 41 | } ``` -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 3 | import { z } from "zod"; 4 | import SwaggerParser from "@apidevtools/swagger-parser"; 5 | import { OpenAPIV3 } from "openapi-types"; 6 | 7 | interface ParsedAPI { 8 | spec: OpenAPIV3.Document; 9 | parser: SwaggerParser; 10 | } 11 | 12 | class SwaggerMCPServer { 13 | private server: McpServer; 14 | private apis: Map<string, ParsedAPI> = new Map(); 15 | 16 | constructor() { 17 | this.server = new McpServer({ 18 | name: "swagger-api-explorer", 19 | version: "1.0.0" 20 | }); 21 | 22 | this.setupResources(); 23 | this.setupTools(); 24 | } 25 | 26 | private setupResources() { 27 | // Resource for loading and caching API specifications 28 | this.server.resource( 29 | "load-api", 30 | new ResourceTemplate("swagger://{apiId}/load", { list: undefined }), 31 | async (uri, params) => { 32 | try { 33 | // Handle both string and string[] cases 34 | const apiId = Array.isArray(params.apiId) ? params.apiId[0] : params.apiId; 35 | 36 | if (!this.apis.has(apiId)) { 37 | return { 38 | contents: [{ 39 | uri: uri.href, 40 | text: `API with ID '${apiId}' not loaded. Use the load_api tool first.`, 41 | mimeType: "text/plain" 42 | }] 43 | }; 44 | } 45 | 46 | const api = this.apis.get(apiId)!; 47 | return { 48 | contents: [{ 49 | uri: uri.href, 50 | text: JSON.stringify({ 51 | info: api.spec.info, 52 | servers: api.spec.servers, 53 | paths: Object.keys(api.spec.paths || {}), 54 | components: Object.keys(api.spec.components?.schemas || {}) 55 | }, null, 2), 56 | mimeType: "application/json" 57 | }] 58 | }; 59 | } catch (error) { 60 | return { 61 | contents: [{ 62 | uri: uri.href, 63 | text: `Error accessing API: ${error instanceof Error ? error.message : String(error)}`, 64 | mimeType: "text/plain" 65 | }] 66 | }; 67 | } 68 | } 69 | ); 70 | 71 | // Resource for getting all available endpoints 72 | this.server.resource( 73 | "endpoints", 74 | new ResourceTemplate("swagger://{apiId}/endpoints", { list: undefined }), 75 | async (uri, params) => { 76 | try { 77 | const apiId = Array.isArray(params.apiId) ? params.apiId[0] : params.apiId; 78 | const api = this.apis.get(apiId); 79 | if (!api) { 80 | return { 81 | contents: [{ 82 | uri: uri.href, 83 | text: `API with ID '${apiId}' not found`, 84 | mimeType: "text/plain" 85 | }] 86 | }; 87 | } 88 | 89 | const endpoints: Array<{method: string, path: string, summary?: string}> = []; 90 | 91 | Object.entries(api.spec.paths || {}).forEach(([path, pathItem]) => { 92 | if (!pathItem) return; 93 | 94 | ['get', 'post', 'put', 'delete', 'patch', 'head', 'options', 'trace'].forEach(method => { 95 | const operation = (pathItem as any)[method] as OpenAPIV3.OperationObject; 96 | if (operation) { 97 | endpoints.push({ 98 | method: method.toUpperCase(), 99 | path, 100 | summary: operation.summary 101 | }); 102 | } 103 | }); 104 | }); 105 | 106 | return { 107 | contents: [{ 108 | uri: uri.href, 109 | text: JSON.stringify(endpoints, null, 2), 110 | mimeType: "application/json" 111 | }] 112 | }; 113 | } catch (error) { 114 | return { 115 | contents: [{ 116 | uri: uri.href, 117 | text: `Error getting endpoints: ${error instanceof Error ? error.message : String(error)}`, 118 | mimeType: "text/plain" 119 | }] 120 | }; 121 | } 122 | } 123 | ); 124 | 125 | // Resource for getting specific endpoint details 126 | this.server.resource( 127 | "endpoint-detail", 128 | new ResourceTemplate("swagger://{apiId}/endpoint/{method}/{path}", { list: undefined }), 129 | async (uri, params) => { 130 | try { 131 | const apiId = Array.isArray(params.apiId) ? params.apiId[0] : params.apiId; 132 | const method = Array.isArray(params.method) ? params.method[0] : params.method; 133 | const path = Array.isArray(params.path) ? params.path[0] : params.path; 134 | 135 | const api = this.apis.get(apiId); 136 | if (!api) { 137 | return { 138 | contents: [{ 139 | uri: uri.href, 140 | text: `API with ID '${apiId}' not found`, 141 | mimeType: "text/plain" 142 | }] 143 | }; 144 | } 145 | 146 | const decodedPath = decodeURIComponent(path); 147 | const pathItem = api.spec.paths?.[decodedPath]; 148 | if (!pathItem) { 149 | return { 150 | contents: [{ 151 | uri: uri.href, 152 | text: `Path '${decodedPath}' not found in API`, 153 | mimeType: "text/plain" 154 | }] 155 | }; 156 | } 157 | 158 | const operation = (pathItem as any)[method.toLowerCase()] as OpenAPIV3.OperationObject; 159 | if (!operation) { 160 | return { 161 | contents: [{ 162 | uri: uri.href, 163 | text: `Method '${method}' not found for path '${decodedPath}'`, 164 | mimeType: "text/plain" 165 | }] 166 | }; 167 | } 168 | 169 | const endpointDetails = this.formatEndpointDetails(operation, decodedPath, method.toUpperCase()); 170 | 171 | return { 172 | contents: [{ 173 | uri: uri.href, 174 | text: JSON.stringify(endpointDetails, null, 2), 175 | mimeType: "application/json" 176 | }] 177 | }; 178 | } catch (error) { 179 | return { 180 | contents: [{ 181 | uri: uri.href, 182 | text: `Error getting endpoint details: ${error instanceof Error ? error.message : String(error)}`, 183 | mimeType: "text/plain" 184 | }] 185 | }; 186 | } 187 | } 188 | ); 189 | } 190 | 191 | private setupTools() { 192 | // Tool to load an API specification 193 | this.server.tool( 194 | "load_api", 195 | { 196 | apiId: z.string().describe("Unique identifier for this API"), 197 | source: z.string().describe("URL or file path to the OpenAPI/Swagger specification") 198 | }, 199 | async ({ apiId, source }) => { 200 | try { 201 | console.error(`Loading API from: ${source}`); 202 | 203 | const parser = new SwaggerParser(); 204 | const spec = await parser.dereference(source) as OpenAPIV3.Document; 205 | 206 | this.apis.set(apiId, { spec, parser }); 207 | 208 | const pathCount = Object.keys(spec.paths || {}).length; 209 | const endpointCount = Object.values(spec.paths || {}).reduce((count, pathItem) => { 210 | if (!pathItem) return count; 211 | return count + ['get', 'post', 'put', 'delete', 'patch', 'head', 'options', 'trace'] 212 | .filter(method => (pathItem as any)[method]).length; 213 | }, 0); 214 | 215 | return { 216 | content: [{ 217 | type: "text", 218 | text: `Successfully loaded API '${spec.info?.title || apiId}' (v${spec.info?.version || 'unknown'}) with ${pathCount} paths and ${endpointCount} endpoints.` 219 | }] 220 | }; 221 | } catch (error) { 222 | return { 223 | content: [{ 224 | type: "text", 225 | text: `Failed to load API: ${error instanceof Error ? error.message : String(error)}` 226 | }], 227 | isError: true 228 | }; 229 | } 230 | } 231 | ); 232 | 233 | // Tool to get endpoint details with optional natural language summary 234 | this.server.tool( 235 | "get_endpoint_details", 236 | { 237 | apiId: z.string().describe("ID of the loaded API"), 238 | method: z.string().describe("HTTP method (GET, POST, etc.)"), 239 | path: z.string().describe("API endpoint path"), 240 | natural: z.boolean().optional().describe("If true, returns a human-readable summary") 241 | }, 242 | async ({ apiId, method, path, natural = false }) => { 243 | try { 244 | const api = this.apis.get(apiId); 245 | if (!api) { 246 | return { 247 | content: [{ 248 | type: "text", 249 | text: `API with ID '${apiId}' not found. Use load_api tool first.` 250 | }], 251 | isError: true 252 | }; 253 | } 254 | 255 | const pathItem = api.spec.paths?.[path]; 256 | if (!pathItem) { 257 | return { 258 | content: [{ 259 | type: "text", 260 | text: `Path '${path}' not found in API` 261 | }], 262 | isError: true 263 | }; 264 | } 265 | 266 | const operation = (pathItem as any)[method.toLowerCase()] as OpenAPIV3.OperationObject; 267 | if (!operation) { 268 | return { 269 | content: [{ 270 | type: "text", 271 | text: `Method '${method}' not found for path '${path}'` 272 | }], 273 | isError: true 274 | }; 275 | } 276 | 277 | const endpointDetails = this.formatEndpointDetails(operation, path, method.toUpperCase()); 278 | 279 | if (natural) { 280 | const summary = this.generateNaturalSummary(endpointDetails, path, method); 281 | return { 282 | content: [{ 283 | type: "text", 284 | text: summary 285 | }] 286 | }; 287 | } 288 | 289 | return { 290 | content: [{ 291 | type: "text", 292 | text: JSON.stringify(endpointDetails, null, 2) 293 | }] 294 | }; 295 | } catch (error) { 296 | return { 297 | content: [{ 298 | type: "text", 299 | text: `Error getting endpoint details: ${error instanceof Error ? error.message : String(error)}` 300 | }], 301 | isError: true 302 | }; 303 | } 304 | } 305 | ); 306 | 307 | // Tool to list all available APIs 308 | this.server.tool( 309 | "list_apis", 310 | {}, 311 | async () => { 312 | const apiList = Array.from(this.apis.entries()).map(([id, api]) => ({ 313 | id, 314 | title: api.spec.info?.title || id, 315 | version: api.spec.info?.version, 316 | description: api.spec.info?.description, 317 | pathCount: Object.keys(api.spec.paths || {}).length 318 | })); 319 | 320 | return { 321 | content: [{ 322 | type: "text", 323 | text: apiList.length > 0 324 | ? JSON.stringify(apiList, null, 2) 325 | : "No APIs loaded. Use the load_api tool to load an API specification." 326 | }] 327 | }; 328 | } 329 | ); 330 | 331 | // Tool to search endpoints by pattern 332 | this.server.tool( 333 | "search_endpoints", 334 | { 335 | apiId: z.string().describe("ID of the loaded API"), 336 | pattern: z.string().describe("Search pattern for endpoint paths or descriptions") 337 | }, 338 | async ({ apiId, pattern }) => { 339 | try { 340 | const api = this.apis.get(apiId); 341 | if (!api) { 342 | return { 343 | content: [{ 344 | type: "text", 345 | text: `API with ID '${apiId}' not found` 346 | }], 347 | isError: true 348 | }; 349 | } 350 | 351 | const matchingEndpoints: Array<{method: string, path: string, summary?: string, description?: string}> = []; 352 | const searchPattern = pattern.toLowerCase(); 353 | 354 | Object.entries(api.spec.paths || {}).forEach(([path, pathItem]) => { 355 | if (!pathItem) return; 356 | 357 | ['get', 'post', 'put', 'delete', 'patch', 'head', 'options', 'trace'].forEach(method => { 358 | const operation = (pathItem as any)[method] as OpenAPIV3.OperationObject; 359 | if (operation) { 360 | const matchesPath = path.toLowerCase().includes(searchPattern); 361 | const matchesSummary = operation.summary?.toLowerCase().includes(searchPattern); 362 | const matchesDescription = operation.description?.toLowerCase().includes(searchPattern); 363 | 364 | if (matchesPath || matchesSummary || matchesDescription) { 365 | matchingEndpoints.push({ 366 | method: method.toUpperCase(), 367 | path, 368 | summary: operation.summary, 369 | description: operation.description 370 | }); 371 | } 372 | } 373 | }); 374 | }); 375 | 376 | return { 377 | content: [{ 378 | type: "text", 379 | text: matchingEndpoints.length > 0 380 | ? JSON.stringify(matchingEndpoints, null, 2) 381 | : `No endpoints found matching pattern: ${pattern}` 382 | }] 383 | }; 384 | } catch (error) { 385 | return { 386 | content: [{ 387 | type: "text", 388 | text: `Error searching endpoints: ${error instanceof Error ? error.message : String(error)}` 389 | }], 390 | isError: true 391 | }; 392 | } 393 | } 394 | ); 395 | } 396 | 397 | private formatEndpointDetails(operation: OpenAPIV3.OperationObject, path: string, method: string) { 398 | const details: any = { 399 | method, 400 | path, 401 | summary: operation.summary, 402 | description: operation.description, 403 | operationId: operation.operationId, 404 | tags: operation.tags, 405 | parameters: [], 406 | requestBody: null, 407 | responses: {} 408 | }; 409 | 410 | // Process parameters 411 | if (operation.parameters) { 412 | details.parameters = operation.parameters.map((param: any) => ({ 413 | name: param.name, 414 | in: param.in, 415 | required: param.required || false, 416 | description: param.description, 417 | schema: param.schema, 418 | example: param.example 419 | })); 420 | } 421 | 422 | // Process request body 423 | if (operation.requestBody) { 424 | const requestBody = operation.requestBody as OpenAPIV3.RequestBodyObject; 425 | details.requestBody = { 426 | description: requestBody.description, 427 | required: requestBody.required || false, 428 | content: requestBody.content 429 | }; 430 | } 431 | 432 | // Process responses 433 | if (operation.responses) { 434 | Object.entries(operation.responses).forEach(([code, response]) => { 435 | if (response && typeof response === 'object' && 'description' in response) { 436 | details.responses[code] = { 437 | description: response.description, 438 | content: (response as OpenAPIV3.ResponseObject).content, 439 | headers: (response as OpenAPIV3.ResponseObject).headers 440 | }; 441 | } 442 | }); 443 | } 444 | 445 | return details; 446 | } 447 | 448 | private generateNaturalSummary(endpointDetails: any, path: string, method: string): string { 449 | const { summary, description, parameters, requestBody, responses } = endpointDetails; 450 | 451 | let naturalSummary = `The ${method} ${path} endpoint`; 452 | 453 | if (summary) { 454 | naturalSummary += ` ${summary.toLowerCase()}`; 455 | } else if (description) { 456 | naturalSummary += ` ${description.toLowerCase()}`; 457 | } 458 | 459 | // Add parameter information 460 | if (parameters && parameters.length > 0) { 461 | const requiredParams = parameters.filter((p: any) => p.required); 462 | const optionalParams = parameters.filter((p: any) => !p.required); 463 | 464 | if (requiredParams.length > 0) { 465 | naturalSummary += `. It requires ${requiredParams.map((p: any) => `${p.name} (${p.in})`).join(', ')}`; 466 | } 467 | 468 | if (optionalParams.length > 0) { 469 | naturalSummary += `. Optional parameters include ${optionalParams.map((p: any) => `${p.name} (${p.in})`).join(', ')}`; 470 | } 471 | } 472 | 473 | // Add request body information 474 | if (requestBody) { 475 | naturalSummary += `. It accepts a request body`; 476 | if (requestBody.required) { 477 | naturalSummary += ' (required)'; 478 | } 479 | } 480 | 481 | // Add response information 482 | const responseKeys = Object.keys(responses || {}); 483 | if (responseKeys.length > 0) { 484 | const successCodes = responseKeys.filter(code => code.startsWith('2')); 485 | if (successCodes.length > 0) { 486 | naturalSummary += `. Success responses include ${successCodes.join(', ')}`; 487 | } 488 | } 489 | 490 | naturalSummary += '.'; 491 | 492 | return naturalSummary; 493 | } 494 | 495 | async connect(transport: StdioServerTransport) { 496 | await this.server.connect(transport); 497 | } 498 | } 499 | 500 | // Main execution 501 | async function main() { 502 | const server = new SwaggerMCPServer(); 503 | const transport = new StdioServerTransport(); 504 | 505 | console.error("Starting Swagger MCP Server..."); 506 | await server.connect(transport); 507 | console.error("Swagger MCP Server running on stdio"); 508 | } 509 | 510 | // Handle graceful shutdown 511 | process.on('SIGINT', () => { 512 | console.error('Received SIGINT, shutting down gracefully...'); 513 | process.exit(0); 514 | }); 515 | 516 | process.on('SIGTERM', () => { 517 | console.error('Received SIGTERM, shutting down gracefully...'); 518 | process.exit(0); 519 | }); 520 | 521 | if (require.main === module) { 522 | main().catch((error) => { 523 | console.error('Fatal error:', error); 524 | process.exit(1); 525 | }); 526 | } ```