# Directory Structure ``` ├── .gitignore ├── example-usage.mjs ├── index.mjs ├── package-lock.json ├── package.json ├── README.md └── sample-petstore.yaml ``` # Files -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` node_modules ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown # OpenAPI Schema Model Context Protocol Server A Model Context Protocol (MCP) server that exposes OpenAPI schema information to Large Language Models (LLMs) like Claude. This server allows an LLM to explore and understand OpenAPI specifications through a set of specialized tools. ## Features - Load any OpenAPI schema file (JSON or YAML) specified via command line argument - Explore API paths, operations, parameters, and schemas - View detailed request and response schemas - Look up component definitions and examples - Search across the entire API specification - Get responses in YAML format for better LLM comprehension ## Usage ### Command Line Run the MCP server with a specific schema file: ```bash # Use the default openapi.yaml in current directory npx -y mcp-openapi-schema # Use a specific schema file (relative path) npx -y mcp-openapi-schema ../petstore.json # Use a specific schema file (absolute path) npx -y mcp-openapi-schema /absolute/path/to/api-spec.yaml # Show help npx -y mcp-openapi-schema --help ``` ### Claude Desktop Integration To use this MCP server with Claude Desktop, edit your `claude_desktop_config.json` configuration file: ```json { "mcpServers": { "OpenAPI Schema": { "command": "npx", "args": ["-y", "mcp-openapi-schema", "/ABSOLUTE/PATH/TO/openapi.yaml"] } } } ``` Location of the configuration file: - macOS/Linux: `~/Library/Application Support/Claude/claude_desktop_config.json` - Windows: `$env:AppData\Claude\claude_desktop_config.json` ### Claude Code Integration To use this MCP server with Claude Code CLI, follow these steps: 1. **Add the OpenAPI Schema MCP server to Claude Code** ```bash # Basic syntax claude mcp add openapi-schema npx -y mcp-openapi-schema # Example with specific schema claude mcp add petstore-api npx -y mcp-openapi-schema ~/Projects/petstore.yaml ``` 2. **Verify the MCP server is registered** ```bash # List all configured servers claude mcp list # Get details for your OpenAPI schema server claude mcp get openapi-schema ``` 3. **Remove the server if needed** ```bash claude mcp remove openapi-schema ``` 4. **Use the tool in Claude Code** Once configured, you can invoke the tool in your Claude Code session by asking questions about the OpenAPI schema. **Tips:** - Use the `-s` or `--scope` flag with `project` (default) or `global` to specify where the configuration is stored - Add multiple MCP servers for different APIs with different names ## MCP Tools The server provides the following tools for LLMs to interact with OpenAPI schemas: - `list-endpoints`: Lists all API paths and their HTTP methods with summaries in a nested object structure - `get-endpoint`: Gets detailed information about a specific endpoint including parameters and responses - `get-request-body`: Gets the request body schema for a specific endpoint and method - `get-response-schema`: Gets the response schema for a specific endpoint, method, and status code - `get-path-parameters`: Gets the parameters for a specific path - `list-components`: Lists all schema components (schemas, responses, parameters, etc.) - `get-component`: Gets detailed definition for a specific component - `list-security-schemes`: Lists all available security schemes - `get-examples`: Gets examples for a specific component or endpoint - `search-schema`: Searches across paths, operations, and schemas ## Examples Example queries to try: ``` What endpoints are available in this API? Show me the details for the POST /pets endpoint. What parameters does the GET /pets/{petId} endpoint take? What is the request body schema for creating a new pet? What response will I get from the DELETE /pets/{petId} endpoint? What schemas are defined in this API? Show me the definition of the Pet schema. What are the available security schemes for this API? Are there any example responses for getting a pet by ID? Search for anything related to "user" in this API. ``` ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json { "name": "mcp-openapi-schema", "version": "0.0.1", "description": "A Model Context Protocol server that exposes OpenAPI schema information to Large Language Models", "type": "module", "main": "index.mjs", "bin": { "mcp-openapi-schema": "index.mjs" }, "scripts": { "start": "node index.mjs", "test": "node example-usage.mjs" }, "keywords": [ "openapi", "swagger", "mcp", "llm", "claude" ], "author": "Hannes Junnila", "license": "MIT", "dependencies": { "@apidevtools/swagger-parser": "^10.1.1", "@modelcontextprotocol/sdk": "^1.7.0", "js-yaml": "^4.1.0", "zod": "^3.24.2" } } ``` -------------------------------------------------------------------------------- /example-usage.mjs: -------------------------------------------------------------------------------- ``` // This is a simple example of how to test the MCP OpenAPI Schema server // using the official MCP SDK client import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; import { dirname, resolve } from "path"; import { fileURLToPath } from "url"; const __dirname = dirname(fileURLToPath(import.meta.url)); // Set up the MCP client to communicate with our server const transport = new StdioClientTransport({ command: "node", args: [ resolve(__dirname, "index.mjs"), resolve(__dirname, "./sample-petstore.yaml") ], }); const client = new Client({ name: "openapi-schema-client", version: "1.0.0", }); // Connect to the server console.log("Connecting to MCP server..."); await client.connect(transport); console.log("Connected to MCP server successfully!"); // Run example tool calls try { // List endpoints console.log("\n--- LISTING ENDPOINTS ---"); const endpoints = await client.callTool({ name: "list-endpoints", arguments: {}, }); console.log(endpoints.content[0].text); // Get endpoint details console.log("\n--- GET ENDPOINT DETAILS ---"); const endpointDetails = await client.callTool({ name: "get-endpoint", arguments: { path: "/pets", method: "get", }, }); console.log(endpointDetails.content[0].text); // Get request body schema console.log("\n--- GET REQUEST BODY SCHEMA ---"); const requestBody = await client.callTool({ name: "get-request-body", arguments: { path: "/pets", method: "post", }, }); console.log(requestBody.content[0].text); // List components console.log("\n--- LIST COMPONENTS ---"); const components = await client.callTool({ name: "list-components", arguments: {}, }); console.log(components.content[0].text); // Get component schema console.log("\n--- GET COMPONENT SCHEMA ---"); const component = await client.callTool({ name: "get-component", arguments: { type: "schemas", name: "Pet", }, }); console.log(component.content[0].text); // Search schema console.log("\n--- SEARCH SCHEMA ---"); const searchResults = await client.callTool({ name: "search-schema", arguments: { pattern: "pet", }, }); console.log(searchResults.content[0].text); // Get path parameters console.log("\n--- GET PATH PARAMETERS ---"); const parameters = await client.callTool({ name: "get-path-parameters", arguments: { path: "/pets/{petId}", method: "get", }, }); console.log(parameters.content[0].text); // Get response schema console.log("\n--- GET RESPONSE SCHEMA ---"); const response = await client.callTool({ name: "get-response-schema", arguments: { path: "/pets/{petId}", method: "get", statusCode: "200", }, }); console.log(response.content[0].text); // List security schemes console.log("\n--- LIST SECURITY SCHEMES ---"); const security = await client.callTool({ name: "list-security-schemes", arguments: {}, }); console.log(security.content[0].text); } catch (error) { console.error("Error during testing:", error); } finally { // Close the connection await client.close(); console.log("\nTests completed, disconnected from server."); } ``` -------------------------------------------------------------------------------- /sample-petstore.yaml: -------------------------------------------------------------------------------- ```yaml openapi: 3.0.0 info: title: Petstore API version: 1.0.0 description: A sample API for managing pets servers: - url: https://petstore.example.com/api/v1 paths: /pets: get: summary: List all pets description: Returns all pets from the system operationId: listPets tags: - pets parameters: - name: limit in: query description: Maximum number of pets to return required: false schema: type: integer format: int32 minimum: 1 maximum: 100 default: 20 responses: '200': description: A paged array of pets content: application/json: schema: $ref: '#/components/schemas/PetsResponse' example: pets: - id: 1 name: "Fluffy" status: "available" category: "cat" - id: 2 name: "Rex" status: "pending" category: "dog" total: 2 limit: 20 offset: 0 post: summary: Create a pet description: Creates a new pet in the store operationId: createPet tags: - pets requestBody: description: Pet to add to the store required: true content: application/json: schema: $ref: '#/components/schemas/NewPet' example: name: "Fluffy" category: "cat" responses: '201': description: Pet created content: application/json: schema: $ref: '#/components/schemas/Pet' example: id: 1 name: "Fluffy" status: "available" category: "cat" '400': description: Invalid input content: application/json: schema: $ref: '#/components/schemas/Error' example: code: 400 message: "Invalid input: name is required" /pets/{petId}: get: summary: Get a pet by ID description: Returns a single pet by ID operationId: getPetById tags: - pets parameters: - name: petId in: path description: ID of pet to return required: true schema: type: integer format: int64 responses: '200': description: Expected response to a valid request content: application/json: schema: $ref: '#/components/schemas/Pet' example: id: 1 name: "Fluffy" status: "available" category: "cat" '404': description: Pet not found content: application/json: schema: $ref: '#/components/schemas/Error' example: code: 404 message: "Pet not found" delete: summary: Delete a pet description: Deletes a pet by ID operationId: deletePet tags: - pets parameters: - name: petId in: path description: ID of pet to delete required: true schema: type: integer format: int64 responses: '204': description: Pet deleted '404': description: Pet not found content: application/json: schema: $ref: '#/components/schemas/Error' components: schemas: Pet: type: object required: - id - name properties: id: type: integer format: int64 description: Unique identifier for the pet name: type: string description: Name of the pet status: type: string description: Status of the pet enum: - available - pending - sold default: available category: type: string description: Category of the pet NewPet: type: object required: - name properties: name: type: string description: Name of the pet category: type: string description: Category of the pet PetsResponse: type: object required: - pets - total properties: pets: type: array items: $ref: '#/components/schemas/Pet' total: type: integer description: Total number of pets limit: type: integer description: Limit used in the request offset: type: integer description: Offset used in the request Error: type: object required: - code - message properties: code: type: integer format: int32 description: Error code message: type: string description: Error message securitySchemes: ApiKeyAuth: type: apiKey in: header name: X-API-Key description: API key for authorization security: - ApiKeyAuth: [] ``` -------------------------------------------------------------------------------- /index.mjs: -------------------------------------------------------------------------------- ``` #!/usr/bin/env node import SwaggerParser from "@apidevtools/swagger-parser"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import yaml from "js-yaml"; import { Console } from "node:console"; import { readFile } from "node:fs/promises"; import { resolve } from "node:path"; import { z } from "zod"; // Redirect console output to stderr to avoid interfering with MCP comms globalThis.console = new Console(process.stderr); const args = process.argv.slice(2); if (args.includes("--help") || args.includes("-h")) { console.log(` OpenAPI Schema Model Context Protocol Server Usage: node index.mjs [path/to/openapi.yaml] Arguments: path/to/openapi.yaml Path to the OpenAPI schema file (JSON or YAML) (optional) If not provided, defaults to openapi.yaml Examples: node index.mjs # Uses default openapi.yaml node index.mjs ../petstore.json # Uses petstore OpenAPI spec node index.mjs /absolute/path/to/api-schema.yaml `); process.exit(0); } const schemaArg = args[0]; const loadSchema = async () => { // Default to openapi.yaml if no argument provided const schemaPath = resolve(schemaArg ?? "openapi.yaml"); try { // Parse and validate the OpenAPI document return await SwaggerParser.validate(schemaPath, { validate: { schema: false } }); } catch (error) { console.error(`Error loading schema: ${error.message}`); process.exit(1); } }; const openApiDoc = await loadSchema(); // Extract schema name from file path or from the OpenAPI info const schemaName = openApiDoc.info?.title || (schemaArg ? schemaArg .split("/") .pop() .replace(/\.(yaml|json)$/i, "") : "openapi"); const server = new McpServer({ name: `OpenAPI Schema: ${schemaName}`, version: "1.0.0", description: `Provides OpenAPI schema information for ${schemaName} (${openApiDoc.info?.version || "unknown version"})`, }); // Helper to convert objects to YAML for better readability const toYaml = (obj) => yaml.dump(obj, { lineWidth: 100, noRefs: true }); // List all API paths and operations server.tool( "list-endpoints", "Lists all API paths and their HTTP methods with summaries, organized by path", () => { const pathMap = {}; for (const [path, pathItem] of Object.entries(openApiDoc.paths || {})) { // Get all HTTP methods for this path const methods = Object.keys(pathItem).filter((key) => ["get", "post", "put", "delete", "patch", "options", "head"].includes(key.toLowerCase()), ); // Create a methods object for this path pathMap[path] = {}; // Add each method with its summary for (const method of methods) { const operation = pathItem[method]; pathMap[path][method.toUpperCase()] = operation.summary || "No summary"; } } return { content: [ { type: "text", text: toYaml(pathMap), }, ], }; }, ); // Get details for a specific endpoint server.tool( "get-endpoint", "Gets detailed information about a specific API endpoint", { path: z.string(), method: z.string() }, ({ path, method }) => { const pathItem = openApiDoc.paths?.[path]; if (!pathItem) { return { content: [{ type: "text", text: `Path ${path} not found` }] }; } const operation = pathItem[method.toLowerCase()]; if (!operation) { return { content: [{ type: "text", text: `Method ${method} not found for path ${path}` }] }; } // Extract relevant information const endpoint = { path, method: method.toUpperCase(), summary: operation.summary, description: operation.description, tags: operation.tags, parameters: operation.parameters, requestBody: operation.requestBody, responses: operation.responses, security: operation.security, deprecated: operation.deprecated, }; return { content: [ { type: "text", text: toYaml(endpoint), }, ], }; }, ); // Get request body schema for a specific endpoint server.tool( "get-request-body", "Gets the request body schema for a specific endpoint", { path: z.string(), method: z.string() }, ({ path, method }) => { const pathItem = openApiDoc.paths?.[path]; if (!pathItem) { return { content: [{ type: "text", text: `Path ${path} not found` }] }; } const operation = pathItem[method.toLowerCase()]; if (!operation) { return { content: [{ type: "text", text: `Method ${method} not found for path ${path}` }] }; } const requestBody = operation.requestBody; if (!requestBody) { return { content: [{ type: "text", text: `No request body defined for ${method} ${path}` }] }; } return { content: [ { type: "text", text: toYaml(requestBody), }, ], }; }, ); // Get response schema for a specific endpoint and status code server.tool( "get-response-schema", "Gets the response schema for a specific endpoint, method, and status code", { path: z.string(), method: z.string(), statusCode: z.string().default("200"), }, ({ path, method, statusCode }) => { const pathItem = openApiDoc.paths?.[path]; if (!pathItem) { return { content: [{ type: "text", text: `Path ${path} not found` }] }; } const operation = pathItem[method.toLowerCase()]; if (!operation) { return { content: [{ type: "text", text: `Method ${method} not found for path ${path}` }] }; } const responses = operation.responses; if (!responses) { return { content: [{ type: "text", text: `No responses defined for ${method} ${path}` }] }; } const response = responses[statusCode] || responses.default; if (!response) { return { content: [ { type: "text", text: `No response for status code ${statusCode} (or default) found for ${method} ${path}.\nAvailable status codes: ${Object.keys(responses).join(", ")}`, }, ], }; } return { content: [ { type: "text", text: toYaml(response), }, ], }; }, ); // Get parameters for a specific path server.tool( "get-path-parameters", "Gets the parameters for a specific path", { path: z.string(), method: z.string().optional() }, ({ path, method }) => { const pathItem = openApiDoc.paths?.[path]; if (!pathItem) { return { content: [{ type: "text", text: `Path ${path} not found` }] }; } let parameters = [...(pathItem.parameters || [])]; // If method is specified, add method-specific parameters if (method) { const operation = pathItem[method.toLowerCase()]; if (operation && operation.parameters) { parameters = [...parameters, ...operation.parameters]; } } if (parameters.length === 0) { return { content: [ { type: "text", text: `No parameters found for ${method ? `${method.toUpperCase()} ` : ""}${path}`, }, ], }; } return { content: [ { type: "text", text: toYaml(parameters), }, ], }; }, ); // List all components server.tool( "list-components", "Lists all schema components (schemas, parameters, responses, etc.)", () => { const components = openApiDoc.components || {}; const result = {}; // For each component type, list the keys for (const [type, items] of Object.entries(components)) { if (items && typeof items === "object") { result[type] = Object.keys(items); } } return { content: [ { type: "text", text: toYaml(result), }, ], }; }, ); // Get a specific component server.tool( "get-component", "Gets detailed definition for a specific component", { type: z.string().describe("Component type (e.g., schemas, parameters, responses)"), name: z.string().describe("Component name"), }, ({ type, name }) => { const components = openApiDoc.components || {}; const componentType = components[type]; if (!componentType) { return { content: [ { type: "text", text: `Component type '${type}' not found. Available types: ${Object.keys(components).join(", ")}`, }, ], }; } const component = componentType[name]; if (!component) { return { content: [ { type: "text", text: `Component '${name}' not found in '${type}'. Available components: ${Object.keys(componentType).join(", ")}`, }, ], }; } return { content: [ { type: "text", text: toYaml(component), }, ], }; }, ); // List security schemes server.tool("list-security-schemes", "Lists all available security schemes", () => { const securitySchemes = openApiDoc.components?.securitySchemes || {}; const result = {}; for (const [name, scheme] of Object.entries(securitySchemes)) { result[name] = { type: scheme.type, description: scheme.description, ...(scheme.type === "oauth2" ? { flows: Object.keys(scheme.flows || {}) } : {}), ...(scheme.type === "apiKey" ? { in: scheme.in, name: scheme.name } : {}), ...(scheme.type === "http" ? { scheme: scheme.scheme } : {}), }; } if (Object.keys(result).length === 0) { return { content: [{ type: "text", text: "No security schemes defined in this API" }] }; } return { content: [ { type: "text", text: toYaml(result), }, ], }; }); // Get examples server.tool( "get-examples", "Gets examples for a specific component or endpoint", { type: z.enum(["request", "response", "component"]).describe("Type of example to retrieve"), path: z.string().optional().describe("API path (required for request/response examples)"), method: z.string().optional().describe("HTTP method (required for request/response examples)"), statusCode: z.string().optional().describe("Status code (for response examples)"), componentType: z .string() .optional() .describe("Component type (required for component examples)"), componentName: z .string() .optional() .describe("Component name (required for component examples)"), }, ({ type, path, method, statusCode, componentType, componentName }) => { if (type === "request") { if (!path || !method) { return { content: [{ type: "text", text: "Path and method are required for request examples" }], }; } const operation = openApiDoc.paths?.[path]?.[method.toLowerCase()]; if (!operation) { return { content: [{ type: "text", text: `Operation ${method.toUpperCase()} ${path} not found` }], }; } if (!operation.requestBody?.content) { return { content: [ { type: "text", text: `No request body defined for ${method.toUpperCase()} ${path}` }, ], }; } const examples = {}; for (const [contentType, content] of Object.entries(operation.requestBody.content)) { if (content.examples) { examples[contentType] = content.examples; } else if (content.example) { examples[contentType] = { default: { value: content.example } }; } } if (Object.keys(examples).length === 0) { return { content: [ { type: "text", text: `No examples found for ${method.toUpperCase()} ${path} request` }, ], }; } return { content: [ { type: "text", text: toYaml(examples), }, ], }; } else if (type === "response") { if (!path || !method) { return { content: [{ type: "text", text: "Path and method are required for response examples" }], }; } const operation = openApiDoc.paths?.[path]?.[method.toLowerCase()]; if (!operation) { return { content: [{ type: "text", text: `Operation ${method.toUpperCase()} ${path} not found` }], }; } if (!operation.responses) { return { content: [ { type: "text", text: `No responses defined for ${method.toUpperCase()} ${path}` }, ], }; } const responseObj = statusCode ? operation.responses[statusCode] : Object.values(operation.responses)[0]; if (!responseObj) { return { content: [ { type: "text", text: `Response ${statusCode} not found for ${method.toUpperCase()} ${path}. Available: ${Object.keys(operation.responses).join(", ")}`, }, ], }; } if (!responseObj.content) { return { content: [{ type: "text", text: `No content defined in response` }] }; } const examples = {}; for (const [contentType, content] of Object.entries(responseObj.content)) { if (content.examples) { examples[contentType] = content.examples; } else if (content.example) { examples[contentType] = { default: { value: content.example } }; } } if (Object.keys(examples).length === 0) { return { content: [ { type: "text", text: `No examples found for ${method.toUpperCase()} ${path} response${statusCode ? ` ${statusCode}` : ""}`, }, ], }; } return { content: [ { type: "text", text: toYaml(examples), }, ], }; } else if (type === "component") { if (!componentType || !componentName) { return { content: [ { type: "text", text: "Component type and name are required for component examples" }, ], }; } const component = openApiDoc.components?.[componentType]?.[componentName]; if (!component) { return { content: [ { type: "text", text: `Component ${componentType}.${componentName} not found` }, ], }; } const examples = component.examples || (component.example ? { default: component.example } : null); if (!examples) { return { content: [ { type: "text", text: `No examples found for component ${componentType}.${componentName}`, }, ], }; } return { content: [ { type: "text", text: toYaml(examples), }, ], }; } }, ); // Search across the API specification server.tool( "search-schema", "Searches across paths, operations, and schemas", { pattern: z.string().describe("Search pattern (case-insensitive)") }, ({ pattern }) => { const searchRegex = new RegExp(pattern, "i"); const results = { paths: [], operations: [], components: [], parameters: [], securitySchemes: [], }; // Search paths for (const path of Object.keys(openApiDoc.paths || {})) { if (searchRegex.test(path)) { results.paths.push(path); } // Search operations within paths const pathItem = openApiDoc.paths[path]; for (const method of ["get", "post", "put", "delete", "patch", "options", "head"]) { const operation = pathItem[method]; if (!operation) continue; if ( searchRegex.test(operation.summary || "") || searchRegex.test(operation.description || "") || (operation.tags && operation.tags.some((tag) => searchRegex.test(tag))) ) { results.operations.push(`${method.toUpperCase()} ${path}`); } // Search parameters for (const param of operation.parameters || []) { if (searchRegex.test(param.name || "") || searchRegex.test(param.description || "")) { results.parameters.push(`${param.name} (${method.toUpperCase()} ${path})`); } } } } // Search components const components = openApiDoc.components || {}; for (const [type, typeObj] of Object.entries(components)) { if (!typeObj || typeof typeObj !== "object") continue; for (const [name, component] of Object.entries(typeObj)) { if (searchRegex.test(name) || searchRegex.test(component.description || "")) { results.components.push(`${type}.${name}`); } } } // Search security schemes for (const [name, scheme] of Object.entries(components.securitySchemes || {})) { if (searchRegex.test(name) || searchRegex.test(scheme.description || "")) { results.securitySchemes.push(name); } } // Clean up empty arrays for (const key of Object.keys(results)) { if (results[key].length === 0) { delete results[key]; } } if (Object.keys(results).length === 0) { return { content: [{ type: "text", text: `No matches found for "${pattern}"` }] }; } return { content: [ { type: "text", text: toYaml(results), }, ], }; }, ); const transport = new StdioServerTransport(); await server.connect(transport); ```