mcp-openapi-schema

#!/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);