Skip to main content
Glama

hypertool-mcp

mcpConfigParser.tsโ€ข14.4 kB
import * as fs from "fs/promises"; import * as path from "path"; import { MCPConfig, ServerConfig, ServerEntry, StdioServerConfig, HttpServerConfig, SSEServerConfig, ExtensionConfig, DxtExtensionConfig, ParseResult, ParserOptions, } from "../types/config.js"; /** * MCP Configuration Parser * Reads and validates .mcp.json configuration files */ export class MCPConfigParser { private options: Required<ParserOptions>; constructor(options: ParserOptions = {}) { this.options = { validatePaths: options.validatePaths ?? true, allowRelativePaths: options.allowRelativePaths ?? true, strict: options.strict ?? false, }; } /** * Parse an MCP configuration file * @param filePath Path to the .mcp.json file * @returns ParseResult with the parsed configuration or errors */ async parseFile(filePath: string): Promise<ParseResult> { try { // Resolve relative paths to absolute paths based on current working directory const resolvedPath = path.resolve(filePath); // Check if file exists await fs.access(resolvedPath); // Read and parse the file const content = await fs.readFile(resolvedPath, "utf-8"); return this.parseContent(content, path.dirname(resolvedPath)); } catch (error) { if ((error as NodeJS.ErrnoException).code === "ENOENT") { return { success: false, error: `Configuration file not found: ${filePath} (resolved to: ${path.resolve(filePath)})`, }; } return { success: false, error: `Failed to read configuration file: ${(error as Error).message}`, }; } } /** * Parse MCP configuration content * @param content JSON content to parse * @param basePath Base path for resolving relative paths * @returns ParseResult with the parsed configuration or errors */ parseContent(content: string, basePath: string = process.cwd()): ParseResult { try { const rawConfig = JSON.parse(content); // Validate the basic structure const validationErrors = this.validateStructure(rawConfig); if (validationErrors.length > 0) { return { success: false, validationErrors, }; } // Parse and validate each server configuration const config: MCPConfig = { mcpServers: {} }; const serverErrors: string[] = []; const serverNames = Object.keys(rawConfig.mcpServers); // Check for potential duplicate server names (case sensitivity, whitespace, etc.) this.validateServerNameUniqueness(serverNames, serverErrors); for (const [serverName, serverConfig] of Object.entries( rawConfig.mcpServers )) { try { const result = this.parseServerConfig( serverName, serverConfig as any, basePath ); if (result.errors.length > 0) { serverErrors.push(...result.errors); if (this.options.strict) { return { success: false, validationErrors: serverErrors, }; } } if (result.config) { config.mcpServers[serverName] = result.config; } } catch (error) { // Log error for this server but continue with others const errorMessage = `Failed to parse config for server "${serverName}": ${(error as Error).message}`; serverErrors.push(errorMessage); console.error(errorMessage); if (this.options.strict) { return { success: false, validationErrors: serverErrors, }; } } } return { success: serverErrors.length === 0, config, validationErrors: serverErrors.length > 0 ? serverErrors : undefined, }; } catch (error) { return { success: false, error: `Invalid JSON: ${(error as Error).message}`, }; } } /** * Validate the basic structure of the configuration * * NOTE: Allow empty server configurations for initial setup */ private validateStructure(rawConfig: any): string[] { const errors: string[] = []; if (!rawConfig || typeof rawConfig !== "object") { errors.push("Configuration must be a valid object"); return errors; } if (!rawConfig.mcpServers || typeof rawConfig.mcpServers !== "object") { errors.push('Configuration must have an "mcpServers" object'); return errors; } return errors; } /** * Parse and validate a single server configuration */ private parseServerConfig( name: string, config: any, basePath: string ): { config?: ServerEntry; errors: string[] } { const errors: string[] = []; if (!config || typeof config !== "object") { errors.push(`Server "${name}" configuration must be an object`); return { errors }; } // Default to stdio type if type field is missing, null, undefined, or empty let serverType = config.type; if ( !serverType || serverType === null || serverType === undefined || (typeof serverType === "string" && serverType.trim() === "") ) { serverType = "stdio"; } // Check if this is an extension config if (serverType === "dxt-extension") { return this.parseExtensionConfig(name, config, basePath); } if ( serverType !== "stdio" && serverType !== "http" && serverType !== "sse" ) { errors.push( `Server "${name}" has invalid type "${serverType}". Must be "stdio", "http", "sse", or "dxt-extension"` ); return { errors }; } // Create a normalized config object with the determined type const normalizedConfig = { ...config, type: serverType }; if (serverType === "stdio") { return this.parseStdioConfig(name, normalizedConfig, basePath); } else if (serverType === "http") { return this.parseHttpConfig(name, normalizedConfig); } else if (serverType === "sse") { return this.parseSSEConfig(name, normalizedConfig); } else { errors.push(`Server "${name}" has unsupported type "${serverType}"`); return { errors }; } } /** * Parse and validate stdio server configuration */ private parseStdioConfig( name: string, config: any, basePath: string ): { config?: StdioServerConfig; errors: string[] } { const errors: string[] = []; if (!config.command || typeof config.command !== "string") { errors.push(`Stdio server "${name}" must have a "command" string`); return { errors }; } if (config.args && !Array.isArray(config.args)) { errors.push(`Stdio server "${name}" args must be an array`); return { errors }; } if (config.args) { for (let i = 0; i < config.args.length; i++) { if (typeof config.args[i] !== "string") { errors.push(`Stdio server "${name}" args[${i}] must be a string`); } } } if ( config.env && (typeof config.env !== "object" || Array.isArray(config.env)) ) { errors.push(`Stdio server "${name}" env must be an object`); return { errors }; } // Validate command path if requested if (this.options.validatePaths) { const commandPath = this.resolveCommandPath(config.command, basePath); if (!commandPath) { errors.push( `Stdio server "${name}" command "${config.command}" not found in PATH or as absolute/relative path` ); } } const stdioConfig: StdioServerConfig = { type: "stdio", command: config.command, args: config.args || [], env: config.env || {}, }; return { config: stdioConfig, errors }; } /** * Parse and validate HTTP server configuration */ private parseHttpConfig( name: string, config: any ): { config?: HttpServerConfig; errors: string[] } { const errors: string[] = []; if (!config.url || typeof config.url !== "string") { errors.push(`HTTP server "${name}" must have a "url" string`); return { errors }; } try { new URL(config.url); } catch { errors.push(`HTTP server "${name}" has invalid URL: ${config.url}`); return { errors }; } if ( config.headers && (typeof config.headers !== "object" || Array.isArray(config.headers)) ) { errors.push(`HTTP server "${name}" headers must be an object`); return { errors }; } if ( config.env && (typeof config.env !== "object" || Array.isArray(config.env)) ) { errors.push(`HTTP server "${name}" env must be an object`); return { errors }; } const httpConfig: HttpServerConfig = { type: "http", url: config.url, headers: config.headers || {}, env: config.env || {}, }; return { config: httpConfig, errors }; } /** * Parse and validate SSE server configuration */ private parseSSEConfig( name: string, config: any ): { config?: SSEServerConfig; errors: string[] } { const errors: string[] = []; if (!config.url || typeof config.url !== "string") { errors.push(`SSE server "${name}" must have a "url" string`); return { errors }; } try { new URL(config.url); } catch { errors.push(`SSE server "${name}" has invalid URL: ${config.url}`); return { errors }; } if ( config.headers && (typeof config.headers !== "object" || Array.isArray(config.headers)) ) { errors.push(`SSE server "${name}" headers must be an object`); return { errors }; } if ( config.env && (typeof config.env !== "object" || Array.isArray(config.env)) ) { errors.push(`SSE server "${name}" env must be an object`); return { errors }; } const sseConfig: SSEServerConfig = { type: "sse", url: config.url, headers: config.headers || {}, env: config.env || {}, }; return { config: sseConfig, errors }; } /** * Parse and validate extension configuration */ private parseExtensionConfig( name: string, config: any, basePath: string ): { config?: ExtensionConfig; errors: string[] } { const errors: string[] = []; if (config.type === "dxt-extension") { return this.parseDxtExtensionConfig(name, config, basePath); } errors.push(`Extension "${name}" has unsupported type "${config.type}"`); return { errors }; } /** * Parse and validate DXT extension configuration */ private parseDxtExtensionConfig( name: string, config: any, basePath: string ): { config?: DxtExtensionConfig; errors: string[] } { const errors: string[] = []; if (!config.path || typeof config.path !== "string") { errors.push(`DXT extension "${name}" must have a "path" string`); return { errors }; } // Resolve relative paths const dxtPath = path.isAbsolute(config.path) ? config.path : path.resolve(basePath, config.path); if ( config.env && (typeof config.env !== "object" || Array.isArray(config.env)) ) { errors.push(`DXT extension "${name}" env must be an object`); return { errors }; } const dxtConfig: DxtExtensionConfig = { type: "dxt-extension", path: dxtPath, env: config.env || {}, }; return { config: dxtConfig, errors }; } /** * Resolve a command path, checking PATH and relative/absolute paths */ private resolveCommandPath(command: string, basePath: string): string | null { // Check if it's an absolute path if (path.isAbsolute(command)) { return command; } // Check if it's a relative path if ( this.options.allowRelativePaths && (command.startsWith("./") || command.startsWith("../")) ) { return path.resolve(basePath, command); } // For commands in PATH, we can't easily validate without executing // Return the command as-is for PATH-based commands return command; } /** * Get a list of server names from a configuration */ static getServerNames(config: MCPConfig): string[] { return Object.keys(config.mcpServers); } /** * Get a specific server entry (server or extension configuration) */ static getServerEntry( config: MCPConfig, serverName: string ): ServerEntry | undefined { return config.mcpServers[serverName]; } /** * Get a specific server configuration (excluding extensions) */ static getServerConfig( config: MCPConfig, serverName: string ): ServerConfig | undefined { const entry = config.mcpServers[serverName]; if (entry && entry.type !== "dxt-extension") { return entry as ServerConfig; } return undefined; } /** * Validate server name uniqueness and catch common naming issues */ private validateServerNameUniqueness( serverNames: string[], errors: string[] ): void { const normalizedNames = new Map<string, string[]>(); for (const name of serverNames) { // Normalize: lowercase, trim whitespace const normalized = name.toLowerCase().trim(); if (!normalizedNames.has(normalized)) { normalizedNames.set(normalized, []); } normalizedNames.get(normalized)!.push(name); } // Check for conflicts for (const [, originalNames] of normalizedNames) { if (originalNames.length > 1) { errors.push( `โŒ Server name conflict detected: Multiple servers with similar names: [${originalNames.join(", ")}].\n` + ` ๐Ÿ’ก Server names must be unique (case-insensitive, whitespace-normalized).\n` + ` ๐Ÿšซ Please rename one of these servers to avoid conflicts.` ); } } // Check for empty or invalid names for (const name of serverNames) { if (!name || typeof name !== "string" || name.trim() === "") { errors.push( `โŒ Invalid server name: Server names cannot be empty or whitespace-only.` ); } if (name !== name.trim()) { errors.push( `โš ๏ธ Server name "${name}" has leading/trailing whitespace. Consider trimming it.` ); } } } }

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/toolprint/hypertool-mcp'

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