Skip to main content
Glama
main.ts15.3 kB
import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema, Tool, } from "@modelcontextprotocol/sdk/types.js"; import { promises as fs } from 'fs'; import path from 'path'; import axios, { AxiosInstance } from 'axios'; import dotenv from 'dotenv'; import winston from 'winston'; // Load environment variables dotenv.config(); // Set up logging - IMPORTANT: MCP servers use stdout for JSON-RPC communication // so we MUST log to stderr to avoid corrupting the protocol messages const logger = winston.createLogger({ level: process.env.LOG_LEVEL || 'info', format: winston.format.combine( winston.format.timestamp(), winston.format.printf(({ timestamp, level, message }: any) => { return `${timestamp} - Hyperfabric_MCP - ${level.toUpperCase()} - ${message}`; }) ), transports: [ new winston.transports.Console({ stderrLevels: ['error', 'warn', 'info', 'debug', 'verbose', 'silly'] }) ] }); interface OpenAPISpec { info: { title: string; version: string; }; paths: Record<string, Record<string, any>>; components?: { schemas?: Record<string, any>; }; } interface OpenAPIOperation { operationId?: string; summary?: string; description?: string; parameters?: Array<{ name: string; in: string; required?: boolean; schema?: any; description?: string; }>; requestBody?: { content?: Record<string, any>; }; responses?: Record<string, any>; } class HyperfabricMCPServer { private server: Server; private httpClient: AxiosInstance; private openApiSpec: OpenAPISpec | null = null; private tools: Tool[] = []; constructor() { this.server = new Server( { name: "Hyperfabric_MCP_API_Server", version: "1.0.0", }, { capabilities: { tools: {}, }, } ); // Initialize HTTP client const token = process.env.HYPERFABRIC_API_TOKEN; if (!token) { logger.error("ERROR: HYPERFABRIC_API_TOKEN not found in environment variables."); process.exit(1); } logger.info(`Using token: ${token.slice(0, 4)}...${token.slice(-4)}`); this.httpClient = axios.create({ baseURL: "https://hyperfabric.cisco.com/api/v1", headers: { "Authorization": `Bearer ${token}`, "Content-Type": "application/json", "Accept": "application/json" } }); this.setupHandlers(); } private async loadOpenAPISpec(): Promise<void> { // Get the spec file path - use environment variable if set, otherwise default to project directory const specFileName = process.env.OPENAPI_SPEC_PATH || 'hf_spec_modified.json'; // Resolve the path relative to the project directory (where package.json is) // When running as MCP server, we need to resolve from the dist directory up to project root const projectRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..'); const specFilePath = path.join(projectRoot, specFileName); try { const specContent = await fs.readFile(specFilePath, 'utf-8'); this.openApiSpec = JSON.parse(specContent); logger.info("✅ OpenAPI spec loaded successfully!"); if (this.openApiSpec?.info?.title) { logger.info(` Title: ${this.openApiSpec.info.title}`); logger.info(` Version: ${this.openApiSpec.info.version}`); } this.generateTools(); } catch (error) { if (error instanceof Error && 'code' in error && error.code === 'ENOENT') { logger.error(`ERROR: The file was not found at '${specFilePath}'`); } else if (error instanceof SyntaxError) { logger.error(`ERROR: The file at '${specFilePath}' is not valid JSON.`); } else { logger.error(`Error loading OpenAPI spec: ${error}`); } process.exit(1); } } private generateTools(): void { if (!this.openApiSpec?.paths) { logger.error("No paths found in OpenAPI spec"); return; } this.tools = []; for (const [pathKey, pathItem] of Object.entries(this.openApiSpec.paths)) { for (const [method, operation] of Object.entries(pathItem)) { if (typeof operation !== 'object' || !operation) continue; const op = operation as OpenAPIOperation; const toolName = this.generateToolName(method, pathKey, op); const tool = this.createToolFromOperation(toolName, method, pathKey, op); if (tool) { this.tools.push(tool); logger.debug(`Generated tool: ${toolName}`); } } } logger.info(`Generated ${this.tools.length} tools from OpenAPI spec`); } private generateToolName(method: string, path: string, operation: OpenAPIOperation): string { if (operation.operationId) { return operation.operationId; } // Generate a name from the method and path const pathParts = path.split('/').filter(part => part && !part.startsWith('{')); const nameBase = pathParts.join('_').replace(/[^a-zA-Z0-9_]/g, '_'); return `${method}_${nameBase}`; } private resolveSchemaRef(schema: any): any { // If schema is a reference, resolve it from components if (schema.$ref && typeof schema.$ref === 'string') { const refPath = schema.$ref.split('/'); let resolved: any = this.openApiSpec; for (const part of refPath) { if (part === '#') continue; resolved = resolved?.[part]; } return resolved || schema; } return schema; } private deepResolveSchema(schema: any, depth: number = 0): any { // Prevent infinite recursion if (depth > 5 || !schema || typeof schema !== 'object') { return schema; } // If this is a reference, resolve it if (schema.$ref && typeof schema.$ref === 'string') { schema = this.resolveSchemaRef(schema); } // Recursively resolve nested properties if (schema.properties && typeof schema.properties === 'object') { for (const [key, prop] of Object.entries(schema.properties)) { (schema.properties as any)[key] = this.deepResolveSchema(prop as any, depth + 1); } } // Recursively resolve items (for arrays) if (schema.items) { schema.items = this.deepResolveSchema(schema.items, depth + 1); } return schema; } private createToolFromOperation( name: string, method: string, path: string, operation: OpenAPIOperation ): Tool | null { let description = operation.summary || operation.description || `${method.toUpperCase()} ${path}`; // Add security context for network port configuration operations if (name === 'nodesSetPorts' || name === 'nodesUpdatePort') { description += '\n\n[SAFE OPERATION] This tool configures network fabric port settings (speed, MTU, VLAN, etc.) via REST API. It does NOT execute code or commands on the system.'; } // Enhance description for create/update operations if (['post', 'put', 'patch'].includes(method.toLowerCase())) { if (method.toLowerCase() === 'post') { description += '\n\nTo use this tool, pass the required fields as direct arguments (e.g., fabrics=[{name:"my-fabric", description:"...", ...}])'; } else if (method.toLowerCase() === 'put') { description += '\n\nTo use this tool, pass the resource ID and the fields to update as arguments'; } else if (method.toLowerCase() === 'patch') { description += '\n\nTo use this tool, pass the resource ID and the fields to patch as arguments'; } } const properties: Record<string, any> = {}; const required: string[] = []; // Process parameters if (operation.parameters) { for (const param of operation.parameters) { if (param.in === 'path' || param.in === 'query') { properties[param.name] = { type: param.schema?.type || 'string', description: param.description || '' }; if (param.required) { required.push(param.name); } } } } // Process request body for POST/PUT/PATCH requests if (operation.requestBody && ['post', 'put', 'patch'].includes(method.toLowerCase())) { // Resolve the requestBody reference if it exists const requestBody = this.resolveSchemaRef(operation.requestBody); const content = requestBody.content; if (content?.['application/json']?.schema) { let schema = content['application/json'].schema; // Deeply resolve schema references schema = this.deepResolveSchema(schema); if (schema.properties) { // Expose the request body properties directly for (const [propName, propSchema] of Object.entries(schema.properties)) { const propDef = propSchema as any; properties[propName] = this.deepResolveSchema(propDef, 0); if (schema.required?.includes(propName)) { required.push(propName); } } } } } return { name, description, inputSchema: { type: 'object', properties, required } }; } private setupHandlers(): void { this.server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: this.tools }; }); this.server.setRequestHandler(CallToolRequestSchema, async (request: any) => { const { name, arguments: args } = request.params; logger.info(`Calling tool: ${name}`); logger.debug(`Tool arguments: ${JSON.stringify(args, null, 2)}`); try { // Find the tool definition const tool = this.tools.find(t => t.name === name); if (!tool) { throw new Error(`Tool ${name} not found`); } // Execute the API call const result = await this.executeApiCall(name, args); return { content: [ { type: "text", text: JSON.stringify(result, null, 2) } ] }; } catch (error) { logger.error(`Error executing tool ${name}:`, error); return { content: [ { type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` } ], isError: true }; } }); } private async executeApiCall(toolName: string, args: any): Promise<any> { // Validate inputs for port configuration tools to prevent misuse if (toolName === 'nodesSetPorts' || toolName === 'nodesUpdatePort') { // Ensure no executable content or shell commands in arguments const argsStr = JSON.stringify(args); if (/(\$\(|`|eval|exec|system|spawn|child_process)/.test(argsStr)) { throw new Error('Security: Invalid arguments detected. Port configuration tools only accept network settings (speed, MTU, VLAN, etc.)'); } } // Extract the original method and path from the tool name // This is a simplified approach - in a production system you'd want a more robust mapping if (!this.openApiSpec?.paths) { throw new Error("OpenAPI spec not loaded"); } // Find the corresponding operation let foundOperation: { method: string; path: string; operation: OpenAPIOperation } | null = null; for (const [pathKey, pathItem] of Object.entries(this.openApiSpec.paths)) { for (const [method, operation] of Object.entries(pathItem)) { if (typeof operation !== 'object' || !operation) continue; const op = operation as OpenAPIOperation; const expectedToolName = this.generateToolName(method, pathKey, op); if (expectedToolName === toolName) { foundOperation = { method, path: pathKey, operation: op }; break; } } if (foundOperation) break; } if (!foundOperation) { throw new Error(`No operation found for tool: ${toolName}`); } // Build the URL by replacing path parameters let url = foundOperation.path; const queryParams: Record<string, string> = {}; // Handle path and query parameters if (foundOperation.operation.parameters) { for (const param of foundOperation.operation.parameters) { const value = args[param.name]; if (value !== undefined) { if (param.in === 'path') { url = url.replace(`{${param.name}}`, encodeURIComponent(value)); } else if (param.in === 'query') { queryParams[param.name] = value; } } } } // Prepare the request const requestConfig: any = { method: foundOperation.method.toUpperCase(), url, params: queryParams, }; // Handle request body for POST/PUT/PATCH if (['post', 'put', 'patch'].includes(foundOperation.method.toLowerCase())) { // Check if args has a requestBody property (legacy format) if (args.requestBody) { requestConfig.data = args.requestBody; } else { // Build request body from exposed properties // This handles cases where schema properties are exposed directly (e.g., fabrics, nodes, etc.) const requestBody: Record<string, any> = {}; const pathItem = this.openApiSpec?.paths?.[foundOperation.path]; const operation = (pathItem as any)?.[foundOperation.method]; if (operation?.requestBody) { const requestBodyDef = this.resolveSchemaRef(operation.requestBody); const schema = this.deepResolveSchema(requestBodyDef.content?.['application/json']?.schema); // Collect all properties that are part of the request body schema if (schema?.properties) { for (const propName of Object.keys(schema.properties)) { if (args.hasOwnProperty(propName)) { requestBody[propName] = args[propName]; } } } } if (Object.keys(requestBody).length > 0) { requestConfig.data = requestBody; } } } logger.debug(`Making API call: ${requestConfig.method} ${url}`); try { const response = await this.httpClient.request(requestConfig); return { status: response.status, statusText: response.statusText, data: response.data }; } catch (error) { if (axios.isAxiosError(error)) { throw new Error(`API call failed: ${error.response?.status} ${error.response?.statusText} - ${JSON.stringify(error.response?.data)}`); } throw error; } } async run(): Promise<void> { await this.loadOpenAPISpec(); const transport = new StdioServerTransport(); await this.server.connect(transport); logger.info("Hyperfabric MCP Server is running..."); } } // Main execution const server = new HyperfabricMCPServer(); server.run().catch((error) => { logger.error("Failed to start server:", error); process.exit(1); });

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/jim-coyne/hyperfabric_MCP'

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