Skip to main content
Glama
SwaggerMcpServer.ts11.3 kB
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { z } from 'zod'; import axios from 'axios'; const SwaggerParser = require('swagger-parser'); import { promises as fs } from 'fs'; import { OpenAPIDocument, ParsedSwaggerDoc, McpToolMetadata } from './types'; import { SwaggerParser as Parser } from './parsers'; import { ToolGenerator } from './generators'; import { AuthManager, ApiProxy } from './proxy'; import { appConfig } from './utils/config'; import { logger } from './utils/logger'; export class SwaggerMcpServer { private server: McpServer; private swaggerDoc?: ParsedSwaggerDoc; private tools: Map<string, McpToolMetadata> = new Map(); private authManager: AuthManager; private apiProxy?: ApiProxy; private refreshInterval?: NodeJS.Timeout; constructor() { this.server = new McpServer({ name: 'swagger-mcp', version: '1.0.0', }); this.authManager = new AuthManager({ type: appConfig.auth.type, token: appConfig.auth.token, header: appConfig.auth.header, apiKeyHeader: appConfig.auth.apiKeyHeader, }); this.setupServer(); } private setupServer(): void { // Server will be initialized when start() is called logger.info('SwaggerMCP server initialized'); } async start(): Promise<void> { logger.info('Starting SwaggerMCP server...'); // Load and parse Swagger document await this.loadSwaggerDocument(); // Set up refresh interval if configured if (appConfig.options.refreshInterval > 0) { this.refreshInterval = setInterval( () => this.loadSwaggerDocument(), appConfig.options.refreshInterval ); logger.info(`Swagger document refresh interval set to ${appConfig.options.refreshInterval}ms`); } // Register tools with MCP server this.registerTools(); // Set up STDIO transport const transport = new StdioServerTransport(); await this.server.connect(transport); logger.info('SwaggerMCP server started successfully'); } private async loadSwaggerDocument(): Promise<void> { try { logger.info('Loading Swagger document...'); let document: OpenAPIDocument; if (appConfig.swagger.url) { // Fetch from URL logger.info(`Fetching Swagger document from URL: ${appConfig.swagger.url}`); const response = await axios.get(appConfig.swagger.url); document = response.data; } else if (appConfig.swagger.path) { // Read from file logger.info(`Reading Swagger document from file: ${appConfig.swagger.path}`); const content = await fs.readFile(appConfig.swagger.path, 'utf-8'); document = JSON.parse(content); } else { throw new Error('No Swagger document source configured'); } // Fix duplicate operationIds before validation this.fixDuplicateOperationIds(document); // Fix host field format issues in Swagger 2.0 documents this.fixHostFieldFormat(document); // Validate and dereference the document const validated = await SwaggerParser.validate(document as any); const dereferenced = await SwaggerParser.dereference(validated as any); // Parse the document const parser = Parser.create(dereferenced); this.swaggerDoc = parser.parse(); // Log the extracted base URL logger.info(`Base URL from Swagger document: ${this.swaggerDoc.baseUrl}`); // Override base URL if configured if (appConfig.api.baseUrl) { logger.info(`Overriding base URL with API_BASE_URL: ${appConfig.api.baseUrl}`); this.swaggerDoc.baseUrl = appConfig.api.baseUrl; } // Create API proxy this.apiProxy = new ApiProxy(this.swaggerDoc.baseUrl, this.authManager); logger.info(`Swagger document loaded: ${this.swaggerDoc.title} (${this.swaggerDoc.version})`); logger.info(`Using API base URL: ${this.swaggerDoc.baseUrl}`); logger.info(`Found ${this.swaggerDoc.operations.length} operations`); // Generate tools from operations this.generateTools(); } catch (error) { logger.error('Failed to load Swagger document:', error); throw error; } } private generateTools(): void { if (!this.swaggerDoc) { return; } this.tools.clear(); const definitions = this.swaggerDoc.securitySchemes; const generator = new ToolGenerator(definitions); const nameCount = new Map<string, number>(); for (const operation of this.swaggerDoc.operations) { try { const tool = generator.generateTool(operation); let finalName = tool.name; // Handle duplicate tool names by adding a suffix if (this.tools.has(finalName)) { const count = nameCount.get(tool.name) || 1; nameCount.set(tool.name, count + 1); finalName = `${tool.name}-${count + 1}`; logger.warn(`Duplicate tool name detected: '${tool.name}' → '${finalName}' (${operation.method} ${operation.path})`); } else { nameCount.set(tool.name, 1); } // Update the tool with the final name const finalTool = { ...tool, name: finalName }; this.tools.set(finalName, finalTool); logger.debug(`Generated tool: ${finalName} (${operation.method} ${operation.path})`); } catch (error) { logger.error(`Failed to generate tool for operation ${operation.operationId}:`, error); } } logger.info(`Generated ${this.tools.size} tools from ${this.swaggerDoc.operations.length} Swagger operations`); } private registerTools(): void { for (const [name, metadata] of this.tools) { this.server.registerTool( name, { title: metadata.title, description: metadata.description, inputSchema: metadata.inputSchema, }, async (params: any) => { return this.executeTool(metadata, params); } ); } logger.info(`Registered ${this.tools.size} tools with MCP server`); } private async executeTool(metadata: McpToolMetadata, params: any): Promise<any> { if (!this.apiProxy) { throw new Error('API proxy not initialized'); } try { logger.info(`Executing tool: ${metadata.name}`); // Validate input parameters const schema = z.object(metadata.inputSchema); const validatedParams = schema.parse(params); // Execute the API operation const result = await this.apiProxy.execute(metadata.operation, validatedParams); // Return result in MCP format return { content: [ { type: 'text', text: JSON.stringify(result, null, 2), }, ], }; } catch (error) { logger.error(`Tool execution failed: ${metadata.name}`, error); // Return error in MCP format return { content: [ { type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}`, }, ], isError: true, }; } } private fixDuplicateOperationIds(document: OpenAPIDocument): void { if (!document.paths) { return; } const operationIds = new Set<string>(); const duplicateCount = new Map<string, number>(); let fixedCount = 0; // First pass: collect all operationIds and identify duplicates for (const [path, pathItem] of Object.entries(document.paths)) { if (!pathItem) continue; const methods = ['get', 'post', 'put', 'delete', 'patch', 'options', 'head'] as const; for (const method of methods) { const operation = pathItem[method]; if (!operation) continue; const operationId = operation.operationId; if (operationId) { if (operationIds.has(operationId)) { // Found duplicate const count = duplicateCount.get(operationId) || 1; duplicateCount.set(operationId, count + 1); } else { operationIds.add(operationId); } } } } // Second pass: fix duplicate operationIds const processedIds = new Set<string>(); for (const [path, pathItem] of Object.entries(document.paths)) { if (!pathItem) continue; const methods = ['get', 'post', 'put', 'delete', 'patch', 'options', 'head'] as const; for (const method of methods) { const operation = pathItem[method]; if (!operation) continue; const originalOperationId = operation.operationId; if (originalOperationId && duplicateCount.has(originalOperationId)) { // This is a duplicate operationId if (processedIds.has(originalOperationId)) { // Generate unique suffix let suffix = 2; let newOperationId = `${originalOperationId}_${suffix}`; while (operationIds.has(newOperationId) || processedIds.has(newOperationId)) { suffix++; newOperationId = `${originalOperationId}_${suffix}`; } operation.operationId = newOperationId; operationIds.add(newOperationId); processedIds.add(newOperationId); fixedCount++; logger.warn(`Fixed duplicate operationId: '${originalOperationId}' → '${newOperationId}' (${method.toUpperCase()} ${path})`); } else { // First occurrence, keep original processedIds.add(originalOperationId); } } } } if (fixedCount > 0) { logger.info(`Fixed ${fixedCount} duplicate operationIds in Swagger document`); } } private fixHostFieldFormat(document: any): void { // This method fixes host field format issues in Swagger 2.0 documents // where the host field contains a path (which should be in basePath instead) if (document.swagger === '2.0' && document.host && typeof document.host === 'string') { const hostValue = document.host; // Check if host contains a path (indicated by '/' character) if (hostValue.includes('/')) { const parts = hostValue.split('/'); const actualHost = parts[0]; const pathPart = '/' + parts.slice(1).join('/'); logger.warn(`Fixing malformed host field: '${hostValue}' → host: '${actualHost}', basePath: '${pathPart}'`); // Update host to contain only domain and port document.host = actualHost; // Combine with existing basePath if present const existingBasePath = document.basePath || ''; const combinedBasePath = pathPart + existingBasePath; // Remove duplicate slashes document.basePath = combinedBasePath.replace(/\/+/g, '/'); logger.info(`Host field fixed - New host: '${document.host}', New basePath: '${document.basePath}'`); } } } async stop(): Promise<void> { logger.info('Stopping SwaggerMCP server...'); if (this.refreshInterval) { clearInterval(this.refreshInterval); } await this.server.close(); logger.info('SwaggerMCP server stopped'); } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/NakiriYuuzu/SwaggerMcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server