#
tokens: 4647/50000 5/5 files
lines: off (toggle) GitHub
raw markdown copy
# Directory Structure

```
├── .gitignore
├── package-lock.json
├── package.json
├── readme.md
├── src
│   └── index.ts
└── tsconfig.json
```

# Files

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
/node_modules
/build
```

--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------

```markdown
# Swagger/OpenAPI MCP Server

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.

## Installation

1. Clone or create the project directory
2. Install dependencies:

```bash
npm install
```

3. Build the TypeScript code:

```bash
npm run build
```

## Usage


### Available Tools

#### `load_api`
Load an OpenAPI/Swagger specification into the server.

**Parameters:**
- `apiId` (string): Unique identifier for this API
- `source` (string): URL or file path to the OpenAPI/Swagger specification

**Example:**
```json
{
  "name": "load_api",
  "arguments": {
    "apiId": "petstore",
    "source": "https://petstore.swagger.io/v2/swagger.json"
  }
}
```

#### `get_endpoint_details`
Get detailed information about a specific API endpoint.

**Parameters:**
- `apiId` (string): ID of the loaded API
- `method` (string): HTTP method (GET, POST, etc.)
- `path` (string): API endpoint path
- `natural` (boolean, optional): If true, returns a human-readable summary

**Example:**
```json
{
  "name": "get_endpoint_details",
  "arguments": {
    "apiId": "petstore",
    "method": "GET",
    "path": "/pet/{petId}",
    "natural": true
  }
}
```

#### `list_apis`
List all currently loaded API specifications.

**Parameters:** None

#### `search_endpoints`
Search for endpoints matching a specific pattern.

**Parameters:**
- `apiId` (string): ID of the loaded API
- `pattern` (string): Search pattern for endpoint paths or descriptions

**Example:**
```json
{
  "name": "search_endpoints",
  "arguments": {
    "apiId": "petstore",
    "pattern": "pet"
  }
}
```

### Available Resources

#### `swagger://{apiId}/load`
Get overview information about a loaded API specification.

#### `swagger://{apiId}/endpoints`
Get a list of all available endpoints for an API.

#### `swagger://{apiId}/endpoint/{method}/{path}`
Get detailed information about a specific endpoint.

## Configuration with Claude Desktop

To use this server with Claude Desktop, add the following to your `claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "swagger-explorer": {
      "command": "node",
      "args": ["/path/to/your/swagger-mcp-server/build/index.js"]
    }
  }
}
```

Replace `/path/to/your/swagger-mcp-server` with the actual path to your project directory.


## License

MIT License

```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./build",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "allowSyntheticDefaultImports": true,
    "resolveJsonModule": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true
  },
  "include": [
    "src/**/*"
  ],
  "exclude": [
    "node_modules",
    "build"
  ]
}
```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
{
  "name": "swagger-mcp-server",
  "version": "1.0.0",
  "description": "MCP Server for exploring Swagger/OpenAPI specifications",
  "main": "build/index.js",
  "bin": {
    "swagger-mcp-server": "./build/index.js"
  },
  "scripts": {
    "build": "tsc && chmod 755 build/index.js",
    "dev": "tsc && node build/index.js",
    "start": "node build/index.js",
    "watch": "tsc --watch"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.0.0",
    "@apidevtools/swagger-parser": "^10.1.0",
    "openapi-types": "^12.1.3",
    "zod": "^3.23.8"
  },
  "devDependencies": {
    "@types/node": "^20.10.0",
    "typescript": "^5.3.0"
  },
  "keywords": [
    "mcp",
    "model-context-protocol",
    "swagger",
    "openapi",
    "api",
    "documentation"
  ],
  "author": "Your Name",
  "license": "MIT",
  "files": [
    "build/"
  ],
  "engines": {
    "node": ">=18.0.0"
  }
}
```

--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------

```typescript
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import SwaggerParser from "@apidevtools/swagger-parser";
import { OpenAPIV3 } from "openapi-types";

interface ParsedAPI {
  spec: OpenAPIV3.Document;
  parser: SwaggerParser;
}

class SwaggerMCPServer {
  private server: McpServer;
  private apis: Map<string, ParsedAPI> = new Map();

  constructor() {
    this.server = new McpServer({
      name: "swagger-api-explorer",
      version: "1.0.0"
    });

    this.setupResources();
    this.setupTools();
  }

  private setupResources() {
    // Resource for loading and caching API specifications
    this.server.resource(
      "load-api",
      new ResourceTemplate("swagger://{apiId}/load", { list: undefined }),
      async (uri, params) => {
        try {
          // Handle both string and string[] cases
          const apiId = Array.isArray(params.apiId) ? params.apiId[0] : params.apiId;
          
          if (!this.apis.has(apiId)) {
            return {
              contents: [{
                uri: uri.href,
                text: `API with ID '${apiId}' not loaded. Use the load_api tool first.`,
                mimeType: "text/plain"
              }]
            };
          }

          const api = this.apis.get(apiId)!;
          return {
            contents: [{
              uri: uri.href,
              text: JSON.stringify({
                info: api.spec.info,
                servers: api.spec.servers,
                paths: Object.keys(api.spec.paths || {}),
                components: Object.keys(api.spec.components?.schemas || {})
              }, null, 2),
              mimeType: "application/json"
            }]
          };
        } catch (error) {
          return {
            contents: [{
              uri: uri.href,
              text: `Error accessing API: ${error instanceof Error ? error.message : String(error)}`,
              mimeType: "text/plain"
            }]
          };
        }
      }
    );

    // Resource for getting all available endpoints
    this.server.resource(
      "endpoints",
      new ResourceTemplate("swagger://{apiId}/endpoints", { list: undefined }),
      async (uri, params) => {
        try {
          const apiId = Array.isArray(params.apiId) ? params.apiId[0] : params.apiId;
          const api = this.apis.get(apiId);
          if (!api) {
            return {
              contents: [{
                uri: uri.href,
                text: `API with ID '${apiId}' not found`,
                mimeType: "text/plain"
              }]
            };
          }

          const endpoints: Array<{method: string, path: string, summary?: string}> = [];
          
          Object.entries(api.spec.paths || {}).forEach(([path, pathItem]) => {
            if (!pathItem) return;
            
            ['get', 'post', 'put', 'delete', 'patch', 'head', 'options', 'trace'].forEach(method => {
              const operation = (pathItem as any)[method] as OpenAPIV3.OperationObject;
              if (operation) {
                endpoints.push({
                  method: method.toUpperCase(),
                  path,
                  summary: operation.summary
                });
              }
            });
          });

          return {
            contents: [{
              uri: uri.href,
              text: JSON.stringify(endpoints, null, 2),
              mimeType: "application/json"
            }]
          };
        } catch (error) {
          return {
            contents: [{
              uri: uri.href,
              text: `Error getting endpoints: ${error instanceof Error ? error.message : String(error)}`,
              mimeType: "text/plain"
            }]
          };
        }
      }
    );

    // Resource for getting specific endpoint details
    this.server.resource(
      "endpoint-detail",
      new ResourceTemplate("swagger://{apiId}/endpoint/{method}/{path}", { list: undefined }),
      async (uri, params) => {
        try {
          const apiId = Array.isArray(params.apiId) ? params.apiId[0] : params.apiId;
          const method = Array.isArray(params.method) ? params.method[0] : params.method;
          const path = Array.isArray(params.path) ? params.path[0] : params.path;
          
          const api = this.apis.get(apiId);
          if (!api) {
            return {
              contents: [{
                uri: uri.href,
                text: `API with ID '${apiId}' not found`,
                mimeType: "text/plain"
              }]
            };
          }

          const decodedPath = decodeURIComponent(path);
          const pathItem = api.spec.paths?.[decodedPath];
          if (!pathItem) {
            return {
              contents: [{
                uri: uri.href,
                text: `Path '${decodedPath}' not found in API`,
                mimeType: "text/plain"
              }]
            };
          }

          const operation = (pathItem as any)[method.toLowerCase()] as OpenAPIV3.OperationObject;
          if (!operation) {
            return {
              contents: [{
                uri: uri.href,
                text: `Method '${method}' not found for path '${decodedPath}'`,
                mimeType: "text/plain"
              }]
            };
          }

          const endpointDetails = this.formatEndpointDetails(operation, decodedPath, method.toUpperCase());

          return {
            contents: [{
              uri: uri.href,
              text: JSON.stringify(endpointDetails, null, 2),
              mimeType: "application/json"
            }]
          };
        } catch (error) {
          return {
            contents: [{
              uri: uri.href,
              text: `Error getting endpoint details: ${error instanceof Error ? error.message : String(error)}`,
              mimeType: "text/plain"
            }]
          };
        }
      }
    );
  }

  private setupTools() {
    // Tool to load an API specification
    this.server.tool(
      "load_api",
      {
        apiId: z.string().describe("Unique identifier for this API"),
        source: z.string().describe("URL or file path to the OpenAPI/Swagger specification")
      },
      async ({ apiId, source }) => {
        try {
          console.error(`Loading API from: ${source}`);
          
          const parser = new SwaggerParser();
          const spec = await parser.dereference(source) as OpenAPIV3.Document;
          
          this.apis.set(apiId, { spec, parser });
          
          const pathCount = Object.keys(spec.paths || {}).length;
          const endpointCount = Object.values(spec.paths || {}).reduce((count, pathItem) => {
            if (!pathItem) return count;
            return count + ['get', 'post', 'put', 'delete', 'patch', 'head', 'options', 'trace']
              .filter(method => (pathItem as any)[method]).length;
          }, 0);

          return {
            content: [{
              type: "text",
              text: `Successfully loaded API '${spec.info?.title || apiId}' (v${spec.info?.version || 'unknown'}) with ${pathCount} paths and ${endpointCount} endpoints.`
            }]
          };
        } catch (error) {
          return {
            content: [{
              type: "text",
              text: `Failed to load API: ${error instanceof Error ? error.message : String(error)}`
            }],
            isError: true
          };
        }
      }
    );

    // Tool to get endpoint details with optional natural language summary
    this.server.tool(
      "get_endpoint_details",
      {
        apiId: z.string().describe("ID of the loaded API"),
        method: z.string().describe("HTTP method (GET, POST, etc.)"),
        path: z.string().describe("API endpoint path"),
        natural: z.boolean().optional().describe("If true, returns a human-readable summary")
      },
      async ({ apiId, method, path, natural = false }) => {
        try {
          const api = this.apis.get(apiId);
          if (!api) {
            return {
              content: [{
                type: "text",
                text: `API with ID '${apiId}' not found. Use load_api tool first.`
              }],
              isError: true
            };
          }

          const pathItem = api.spec.paths?.[path];
          if (!pathItem) {
            return {
              content: [{
                type: "text",
                text: `Path '${path}' not found in API`
              }],
              isError: true
            };
          }

          const operation = (pathItem as any)[method.toLowerCase()] as OpenAPIV3.OperationObject;
          if (!operation) {
            return {
              content: [{
                type: "text",
                text: `Method '${method}' not found for path '${path}'`
              }],
              isError: true
            };
          }

          const endpointDetails = this.formatEndpointDetails(operation, path, method.toUpperCase());

          if (natural) {
            const summary = this.generateNaturalSummary(endpointDetails, path, method);
            return {
              content: [{
                type: "text",
                text: summary
              }]
            };
          }

          return {
            content: [{
              type: "text",
              text: JSON.stringify(endpointDetails, null, 2)
            }]
          };
        } catch (error) {
          return {
            content: [{
              type: "text",
              text: `Error getting endpoint details: ${error instanceof Error ? error.message : String(error)}`
            }],
            isError: true
          };
        }
      }
    );

    // Tool to list all available APIs
    this.server.tool(
      "list_apis",
      {},
      async () => {
        const apiList = Array.from(this.apis.entries()).map(([id, api]) => ({
          id,
          title: api.spec.info?.title || id,
          version: api.spec.info?.version,
          description: api.spec.info?.description,
          pathCount: Object.keys(api.spec.paths || {}).length
        }));

        return {
          content: [{
            type: "text",
            text: apiList.length > 0 
              ? JSON.stringify(apiList, null, 2)
              : "No APIs loaded. Use the load_api tool to load an API specification."
          }]
        };
      }
    );

    // Tool to search endpoints by pattern
    this.server.tool(
      "search_endpoints",
      {
        apiId: z.string().describe("ID of the loaded API"),
        pattern: z.string().describe("Search pattern for endpoint paths or descriptions")
      },
      async ({ apiId, pattern }) => {
        try {
          const api = this.apis.get(apiId);
          if (!api) {
            return {
              content: [{
                type: "text",
                text: `API with ID '${apiId}' not found`
              }],
              isError: true
            };
          }

          const matchingEndpoints: Array<{method: string, path: string, summary?: string, description?: string}> = [];
          const searchPattern = pattern.toLowerCase();

          Object.entries(api.spec.paths || {}).forEach(([path, pathItem]) => {
            if (!pathItem) return;
            
            ['get', 'post', 'put', 'delete', 'patch', 'head', 'options', 'trace'].forEach(method => {
              const operation = (pathItem as any)[method] as OpenAPIV3.OperationObject;
              if (operation) {
                const matchesPath = path.toLowerCase().includes(searchPattern);
                const matchesSummary = operation.summary?.toLowerCase().includes(searchPattern);
                const matchesDescription = operation.description?.toLowerCase().includes(searchPattern);
                
                if (matchesPath || matchesSummary || matchesDescription) {
                  matchingEndpoints.push({
                    method: method.toUpperCase(),
                    path,
                    summary: operation.summary,
                    description: operation.description
                  });
                }
              }
            });
          });

          return {
            content: [{
              type: "text",
              text: matchingEndpoints.length > 0
                ? JSON.stringify(matchingEndpoints, null, 2)
                : `No endpoints found matching pattern: ${pattern}`
            }]
          };
        } catch (error) {
          return {
            content: [{
              type: "text",
              text: `Error searching endpoints: ${error instanceof Error ? error.message : String(error)}`
            }],
            isError: true
          };
        }
      }
    );
  }

  private formatEndpointDetails(operation: OpenAPIV3.OperationObject, path: string, method: string) {
    const details: any = {
      method,
      path,
      summary: operation.summary,
      description: operation.description,
      operationId: operation.operationId,
      tags: operation.tags,
      parameters: [],
      requestBody: null,
      responses: {}
    };

    // Process parameters
    if (operation.parameters) {
      details.parameters = operation.parameters.map((param: any) => ({
        name: param.name,
        in: param.in,
        required: param.required || false,
        description: param.description,
        schema: param.schema,
        example: param.example
      }));
    }

    // Process request body
    if (operation.requestBody) {
      const requestBody = operation.requestBody as OpenAPIV3.RequestBodyObject;
      details.requestBody = {
        description: requestBody.description,
        required: requestBody.required || false,
        content: requestBody.content
      };
    }

    // Process responses
    if (operation.responses) {
      Object.entries(operation.responses).forEach(([code, response]) => {
        if (response && typeof response === 'object' && 'description' in response) {
          details.responses[code] = {
            description: response.description,
            content: (response as OpenAPIV3.ResponseObject).content,
            headers: (response as OpenAPIV3.ResponseObject).headers
          };
        }
      });
    }

    return details;
  }

  private generateNaturalSummary(endpointDetails: any, path: string, method: string): string {
    const { summary, description, parameters, requestBody, responses } = endpointDetails;
    
    let naturalSummary = `The ${method} ${path} endpoint`;
    
    if (summary) {
      naturalSummary += ` ${summary.toLowerCase()}`;
    } else if (description) {
      naturalSummary += ` ${description.toLowerCase()}`;
    }
    
    // Add parameter information
    if (parameters && parameters.length > 0) {
      const requiredParams = parameters.filter((p: any) => p.required);
      const optionalParams = parameters.filter((p: any) => !p.required);
      
      if (requiredParams.length > 0) {
        naturalSummary += `. It requires ${requiredParams.map((p: any) => `${p.name} (${p.in})`).join(', ')}`;
      }
      
      if (optionalParams.length > 0) {
        naturalSummary += `. Optional parameters include ${optionalParams.map((p: any) => `${p.name} (${p.in})`).join(', ')}`;
      }
    }
    
    // Add request body information
    if (requestBody) {
      naturalSummary += `. It accepts a request body`;
      if (requestBody.required) {
        naturalSummary += ' (required)';
      }
    }
    
    // Add response information
    const responseKeys = Object.keys(responses || {});
    if (responseKeys.length > 0) {
      const successCodes = responseKeys.filter(code => code.startsWith('2'));
      if (successCodes.length > 0) {
        naturalSummary += `. Success responses include ${successCodes.join(', ')}`;
      }
    }
    
    naturalSummary += '.';
    
    return naturalSummary;
  }

  async connect(transport: StdioServerTransport) {
    await this.server.connect(transport);
  }
}

// Main execution
async function main() {
  const server = new SwaggerMCPServer();
  const transport = new StdioServerTransport();
  
  console.error("Starting Swagger MCP Server...");
  await server.connect(transport);
  console.error("Swagger MCP Server running on stdio");
}

// Handle graceful shutdown
process.on('SIGINT', () => {
  console.error('Received SIGINT, shutting down gracefully...');
  process.exit(0);
});

process.on('SIGTERM', () => {
  console.error('Received SIGTERM, shutting down gracefully...');
  process.exit(0);
});

if (require.main === module) {
  main().catch((error) => {
    console.error('Fatal error:', error);
    process.exit(1);
  });
}
```