Skip to main content
Glama

hypertool-mcp

mcpConfigParser.test.tsโ€ข30.8 kB
import { describe, it, expect, beforeEach, vi } from "vitest"; import { MCPConfigParser } from "./mcpConfigParser.js"; import { MCPConfig } from "../types/config.js"; import * as fs from "fs/promises"; import * as path from "path"; // Mock fs module vi.mock("fs/promises"); const mockFs = fs as vi.Mocked<typeof fs>; describe("MCPConfigParser", () => { let parser: MCPConfigParser; beforeEach(() => { parser = new MCPConfigParser(); vi.clearAllMocks(); }); describe("parseContent", () => { it("should parse valid configuration with stdio server", () => { const content = JSON.stringify({ mcpServers: { git: { type: "stdio", command: "uvx", args: ["mcp-server-git"], }, }, }); const result = parser.parseContent(content); expect(result.success).toBe(true); expect(result.config).toBeDefined(); expect(result.config?.mcpServers.git).toMatchObject({ type: "stdio", command: "uvx", args: ["mcp-server-git"], env: {}, }); }); it("should parse valid configuration with SSE server", () => { const content = JSON.stringify({ mcpServers: { context7: { type: "sse", url: "https://sse.context7.com/sse", }, }, }); const result = parser.parseContent(content); expect(result.success).toBe(true); expect(result.config).toBeDefined(); expect(result.config?.mcpServers.context7).toMatchObject({ type: "sse", url: "https://sse.context7.com/sse", headers: {}, env: {}, }); }); it("should parse configuration with multiple servers", () => { const content = JSON.stringify({ mcpServers: { git: { type: "stdio", command: "uvx", args: ["mcp-server-git"], }, docker: { type: "stdio", command: "uvx", args: ["mcp-server-docker"], env: { DOCKER_HOST: "unix:///var/run/docker.sock" }, }, context7: { type: "sse", url: "https://sse.context7.com/sse", }, }, }); const result = parser.parseContent(content); expect(result.success).toBe(true); expect(result.config).toBeDefined(); expect(Object.keys(result.config!.mcpServers)).toHaveLength(3); }); it("should handle invalid JSON", () => { const content = "{ invalid json }"; const result = parser.parseContent(content); expect(result.success).toBe(false); expect(result.error).toContain("Invalid JSON"); }); it("should validate missing mcpServers field", () => { const content = JSON.stringify({}); const result = parser.parseContent(content); expect(result.success).toBe(false); expect(result.validationErrors).toContain( 'Configuration must have an "mcpServers" object' ); }); it("should allow empty mcpServers", () => { const content = JSON.stringify({ mcpServers: {}, }); const result = parser.parseContent(content); expect(result.success).toBe(true); expect(result.config).toEqual({ mcpServers: {}, }); }); it("should default to stdio type when type field is missing (legacy behavior test)", () => { const content = JSON.stringify({ mcpServers: { git: { command: "uvx", args: ["mcp-server-git"], }, }, }); const result = parser.parseContent(content); expect(result.success).toBe(true); expect(result.config?.mcpServers.git).toMatchObject({ type: "stdio", command: "uvx", args: ["mcp-server-git"], env: {}, }); }); it("should validate invalid server type", () => { const content = JSON.stringify({ mcpServers: { git: { type: "invalid", }, }, }); const result = parser.parseContent(content); expect(result.success).toBe(false); expect(result.validationErrors).toContain( 'Server "git" has invalid type "invalid". Must be "stdio", "http", "sse", or "dxt-extension"' ); }); it("should validate missing command for stdio server", () => { const content = JSON.stringify({ mcpServers: { git: { type: "stdio", }, }, }); const result = parser.parseContent(content); expect(result.success).toBe(false); expect(result.validationErrors).toContain( 'Stdio server "git" must have a "command" string' ); }); it("should validate missing url for SSE server", () => { const content = JSON.stringify({ mcpServers: { api: { type: "sse", }, }, }); const result = parser.parseContent(content); expect(result.success).toBe(false); expect(result.validationErrors).toContain( 'SSE server "api" must have a "url" string' ); }); it("should validate invalid URL for SSE server", () => { const content = JSON.stringify({ mcpServers: { api: { type: "sse", url: "not-a-valid-url", }, }, }); const result = parser.parseContent(content); expect(result.success).toBe(false); expect(result.validationErrors).toContain( 'SSE server "api" has invalid URL: not-a-valid-url' ); }); it("should validate args must be array for stdio server", () => { const content = JSON.stringify({ mcpServers: { git: { type: "stdio", command: "uvx", args: "not-an-array", }, }, }); const result = parser.parseContent(content); expect(result.success).toBe(false); expect(result.validationErrors).toContain( 'Stdio server "git" args must be an array' ); }); it("should validate args items must be strings", () => { const content = JSON.stringify({ mcpServers: { git: { type: "stdio", command: "uvx", args: ["mcp-server-git", 123, true], }, }, }); const result = parser.parseContent(content); expect(result.success).toBe(false); expect(result.validationErrors).toContain( 'Stdio server "git" args[1] must be a string' ); expect(result.validationErrors).toContain( 'Stdio server "git" args[2] must be a string' ); }); it("should handle SSE server with headers", () => { const content = JSON.stringify({ mcpServers: { api: { type: "sse", url: "https://api.example.com/sse", headers: { Authorization: "Bearer token", "X-Custom-Header": "value", }, }, }, }); const result = parser.parseContent(content); expect(result.success).toBe(true); expect(result.config?.mcpServers.api).toMatchObject({ type: "sse", url: "https://api.example.com/sse", headers: { Authorization: "Bearer token", "X-Custom-Header": "value", }, }); }); it("should handle partial errors in non-strict mode", () => { const parser = new MCPConfigParser({ strict: false }); const content = JSON.stringify({ mcpServers: { git: { type: "stdio", command: "uvx", args: ["mcp-server-git"], }, invalid: { type: "stdio", // missing command }, }, }); const result = parser.parseContent(content); expect(result.success).toBe(false); expect(result.config?.mcpServers.git).toBeDefined(); expect(result.config?.mcpServers.invalid).toBeUndefined(); expect(result.validationErrors).toContain( 'Stdio server "invalid" must have a "command" string' ); }); it("should continue parsing when one server has invalid configuration", () => { const parser = new MCPConfigParser({ strict: false }); const content = JSON.stringify({ mcpServers: { good1: { type: "stdio", command: "uvx", args: ["mcp-server-1"], }, malformed: { type: "stdio", // Missing required command field }, good2: { type: "sse", url: "https://example.com/sse", }, }, }); const result = parser.parseContent(content); expect(result.success).toBe(false); expect(result.config?.mcpServers.good1).toBeDefined(); expect(result.config?.mcpServers.good2).toBeDefined(); expect(result.config?.mcpServers.malformed).toBeUndefined(); expect(result.validationErrors).toContain( 'Stdio server "malformed" must have a "command" string' ); }); it("should handle mix of missing types and invalid configs", () => { const parser = new MCPConfigParser({ strict: false }); const consoleSpy = vi .spyOn(console, "error") .mockImplementation(() => {}); const content = JSON.stringify({ mcpServers: { missingType: { // Missing type field - should fail with new validation command: "uvx", args: ["mcp-server-git"], }, invalidStdio: { type: "stdio", // Missing required command }, validSse: { type: "sse", url: "https://example.com/sse", }, }, }); const result = parser.parseContent(content); expect(result.success).toBe(false); expect(result.config?.mcpServers.missingType).toBeDefined(); // Should now be defined with defaulted type expect(result.config?.mcpServers.invalidStdio).toBeUndefined(); expect(result.config?.mcpServers.validSse).toBeDefined(); // missingType should now be valid with defaulted type expect(result.validationErrors).toContain( 'Stdio server "invalidStdio" must have a "command" string' ); consoleSpy.mockRestore(); }); it("should fail completely in strict mode with any error", () => { const parser = new MCPConfigParser({ strict: true }); const content = JSON.stringify({ mcpServers: { git: { type: "stdio", command: "uvx", args: ["mcp-server-git"], }, invalid: { type: "stdio", // missing command }, }, }); const result = parser.parseContent(content); expect(result.success).toBe(false); expect(result.config).toBeUndefined(); expect(result.validationErrors).toContain( 'Stdio server "invalid" must have a "command" string' ); }); }); describe("parseFile", () => { it("should read and parse file successfully", async () => { const content = JSON.stringify({ mcpServers: { git: { type: "stdio", command: "uvx", args: ["mcp-server-git"], }, }, }); mockFs.access.mockResolvedValue(undefined); mockFs.readFile.mockResolvedValue(content); const result = await parser.parseFile("/path/to/.mcp.json"); expect(result.success).toBe(true); expect(result.config).toBeDefined(); expect(mockFs.access).toHaveBeenCalledWith("/path/to/.mcp.json"); expect(mockFs.readFile).toHaveBeenCalledWith( "/path/to/.mcp.json", "utf-8" ); }); it("should resolve relative paths to absolute paths", async () => { const relativePath = "./test-config.json"; const expectedAbsolutePath = path.resolve(relativePath); const content = JSON.stringify({ mcpServers: { git: { type: "stdio", command: "uvx", args: ["mcp-server-git"], }, }, }); mockFs.access.mockResolvedValue(undefined); mockFs.readFile.mockResolvedValue(content); const result = await parser.parseFile(relativePath); expect(result.success).toBe(true); expect(result.config).toBeDefined(); expect(mockFs.access).toHaveBeenCalledWith(expectedAbsolutePath); expect(mockFs.readFile).toHaveBeenCalledWith( expectedAbsolutePath, "utf-8" ); }); it("should handle relative paths with parent directories", async () => { const relativePath = "../configs/mcp.json"; const expectedAbsolutePath = path.resolve(relativePath); const content = JSON.stringify({ mcpServers: { test: { type: "sse", url: "https://example.com/sse", }, }, }); mockFs.access.mockResolvedValue(undefined); mockFs.readFile.mockResolvedValue(content); const result = await parser.parseFile(relativePath); expect(result.success).toBe(true); expect(result.config).toBeDefined(); expect(mockFs.access).toHaveBeenCalledWith(expectedAbsolutePath); expect(mockFs.readFile).toHaveBeenCalledWith( expectedAbsolutePath, "utf-8" ); }); it("should handle file not found error", async () => { const error = new Error("ENOENT") as NodeJS.ErrnoException; error.code = "ENOENT"; mockFs.access.mockRejectedValue(error); const result = await parser.parseFile("/path/to/missing.json"); expect(result.success).toBe(false); expect(result.error).toContain("Configuration file not found"); }); it("should handle file not found error with relative paths", async () => { const relativePath = "./missing-config.json"; const expectedAbsolutePath = path.resolve(relativePath); const error = new Error("ENOENT") as NodeJS.ErrnoException; error.code = "ENOENT"; mockFs.access.mockRejectedValue(error); const result = await parser.parseFile(relativePath); expect(result.success).toBe(false); expect(result.error).toContain("Configuration file not found"); expect(result.error).toContain(relativePath); expect(result.error).toContain(expectedAbsolutePath); }); it("should handle file read error", async () => { mockFs.access.mockResolvedValue(undefined); mockFs.readFile.mockRejectedValue(new Error("Permission denied")); const result = await parser.parseFile("/path/to/forbidden.json"); expect(result.success).toBe(false); expect(result.error).toContain( "Failed to read configuration file: Permission denied" ); }); }); describe("static methods", () => { it("should get server names", () => { const config: MCPConfig = { mcpServers: { git: { type: "stdio", command: "uvx", args: ["mcp-server-git"] }, docker: { type: "stdio", command: "uvx", args: ["mcp-server-docker"], }, }, }; const names = MCPConfigParser.getServerNames(config); expect(names).toEqual(["git", "docker"]); }); it("should get specific server config", () => { const config: MCPConfig = { mcpServers: { git: { type: "stdio", command: "uvx", args: ["mcp-server-git"] }, docker: { type: "stdio", command: "uvx", args: ["mcp-server-docker"], }, }, }; const gitConfig = MCPConfigParser.getServerConfig(config, "git"); const missingConfig = MCPConfigParser.getServerConfig(config, "missing"); expect(gitConfig).toEqual(config.mcpServers.git); expect(missingConfig).toBeUndefined(); }); }); describe("path validation", () => { it("should skip path validation when disabled", () => { const parser = new MCPConfigParser({ validatePaths: false }); const content = JSON.stringify({ mcpServers: { custom: { type: "stdio", command: "/non/existent/path", }, }, }); const result = parser.parseContent(content); expect(result.success).toBe(true); }); it("should handle relative paths when allowed", () => { const parser = new MCPConfigParser({ allowRelativePaths: true }); const content = JSON.stringify({ mcpServers: { local: { type: "stdio", command: "./bin/sse-server", }, }, }); const result = parser.parseContent(content); expect(result.success).toBe(true); expect(result.config?.mcpServers.local).toMatchObject({ type: "stdio", command: "./bin/sse-server", }); }); }); describe("optional type field with stdio default", () => { it("should default to stdio type when type field is missing", () => { const content = JSON.stringify({ mcpServers: { git: { command: "uvx", args: ["mcp-server-git"], }, }, }); const result = parser.parseContent(content); expect(result.success).toBe(true); expect(result.config).toBeDefined(); expect(result.config?.mcpServers.git).toMatchObject({ type: "stdio", command: "uvx", args: ["mcp-server-git"], env: {}, }); }); it("should default to stdio type when type field is undefined", () => { const content = JSON.stringify({ mcpServers: { docker: { type: undefined, command: "docker-mcp", args: ["--port", "8080"], env: { DOCKER_HOST: "unix:///var/run/docker.sock" }, }, }, }); const result = parser.parseContent(content); expect(result.success).toBe(true); expect(result.config?.mcpServers.docker).toMatchObject({ type: "stdio", command: "docker-mcp", args: ["--port", "8080"], env: { DOCKER_HOST: "unix:///var/run/docker.sock" }, }); }); it("should default to stdio type when type field is null", () => { const content = JSON.stringify({ mcpServers: { filesystem: { type: null, command: "mcp-server-filesystem", args: ["/home/user/projects"], }, }, }); const result = parser.parseContent(content); expect(result.success).toBe(true); expect(result.config?.mcpServers.filesystem).toMatchObject({ type: "stdio", command: "mcp-server-filesystem", args: ["/home/user/projects"], env: {}, }); }); it("should default to stdio type when type field is empty string", () => { const content = JSON.stringify({ mcpServers: { calculator: { type: "", command: "python", args: ["-m", "calculator_mcp"], }, }, }); const result = parser.parseContent(content); expect(result.success).toBe(true); expect(result.config?.mcpServers.calculator).toMatchObject({ type: "stdio", command: "python", args: ["-m", "calculator_mcp"], env: {}, }); }); it("should handle multiple servers with missing type fields", () => { const content = JSON.stringify({ mcpServers: { git: { command: "uvx", args: ["mcp-server-git"], }, docker: { command: "uvx", args: ["mcp-server-docker"], env: { DOCKER_HOST: "unix:///var/run/docker.sock" }, }, api: { type: "sse", url: "https://api.example.com/sse", }, }, }); const result = parser.parseContent(content); expect(result.success).toBe(true); expect(result.config).toBeDefined(); expect(Object.keys(result.config!.mcpServers)).toHaveLength(3); expect(result.config?.mcpServers.git).toMatchObject({ type: "stdio", command: "uvx", args: ["mcp-server-git"], env: {}, }); expect(result.config?.mcpServers.docker).toMatchObject({ type: "stdio", command: "uvx", args: ["mcp-server-docker"], env: { DOCKER_HOST: "unix:///var/run/docker.sock" }, }); expect(result.config?.mcpServers.api).toMatchObject({ type: "sse", url: "https://api.example.com/sse", headers: {}, env: {}, }); }); it("should still validate stdio requirements when type is defaulted", () => { const content = JSON.stringify({ mcpServers: { missingCommand: { // Missing command - should fail validation args: ["--verbose"], }, }, }); const result = parser.parseContent(content); expect(result.success).toBe(false); expect(result.validationErrors).toContain( 'Stdio server "missingCommand" must have a "command" string' ); }); it("should validate args array when type is defaulted", () => { const content = JSON.stringify({ mcpServers: { invalidArgs: { command: "uvx", args: "not-an-array", }, }, }); const result = parser.parseContent(content); expect(result.success).toBe(false); expect(result.validationErrors).toContain( 'Stdio server "invalidArgs" args must be an array' ); }); it("should validate args items are strings when type is defaulted", () => { const content = JSON.stringify({ mcpServers: { mixedArgs: { command: "python", args: ["-m", "server", 123, true, null], }, }, }); const result = parser.parseContent(content); expect(result.success).toBe(false); expect(result.validationErrors).toContain( 'Stdio server "mixedArgs" args[2] must be a string' ); expect(result.validationErrors).toContain( 'Stdio server "mixedArgs" args[3] must be a string' ); expect(result.validationErrors).toContain( 'Stdio server "mixedArgs" args[4] must be a string' ); }); }); describe("graceful handling of individual config failures", () => { it("should continue processing valid configs when one has missing type and invalid command", () => { const parser = new MCPConfigParser({ strict: false }); const content = JSON.stringify({ mcpServers: { validServer: { type: "stdio", command: "uvx", args: ["mcp-server-git"], }, invalidServer: { // Missing type (should default to stdio) but also missing command args: ["--verbose"], }, anotherValidServer: { type: "sse", url: "https://example.com/sse", }, }, }); const result = parser.parseContent(content); expect(result.success).toBe(false); expect(result.config?.mcpServers.validServer).toBeDefined(); expect(result.config?.mcpServers.anotherValidServer).toBeDefined(); expect(result.config?.mcpServers.invalidServer).toBeUndefined(); expect(result.validationErrors).toContain( 'Stdio server "invalidServer" must have a "command" string' ); expect(Object.keys(result.config!.mcpServers)).toHaveLength(2); }); it("should handle mix of missing types and explicit types with errors", () => { const parser = new MCPConfigParser({ strict: false }); const content = JSON.stringify({ mcpServers: { noTypeValid: { // Should default to stdio and be valid command: "uvx", args: ["mcp-server-1"], }, noTypeInvalid: { // Should default to stdio but missing command args: ["--config", "path"], }, explicitStdioInvalid: { type: "stdio", // Missing command }, explicitSseInvalid: { type: "sse", // Missing URL }, explicitSseValid: { type: "sse", url: "https://api.example.com/sse", }, }, }); const result = parser.parseContent(content); expect(result.success).toBe(false); expect(result.config?.mcpServers.noTypeValid).toBeDefined(); expect(result.config?.mcpServers.explicitSseValid).toBeDefined(); expect(result.config?.mcpServers.noTypeInvalid).toBeUndefined(); expect(result.config?.mcpServers.explicitStdioInvalid).toBeUndefined(); expect(result.config?.mcpServers.explicitSseInvalid).toBeUndefined(); expect(result.validationErrors).toContain( 'Stdio server "noTypeInvalid" must have a "command" string' ); expect(result.validationErrors).toContain( 'Stdio server "explicitStdioInvalid" must have a "command" string' ); expect(result.validationErrors).toContain( 'SSE server "explicitSseInvalid" must have a "url" string' ); expect(Object.keys(result.config!.mcpServers)).toHaveLength(2); }); it("should log warnings for invalid configs in non-strict mode", () => { const parser = new MCPConfigParser({ strict: false }); const consoleSpy = vi .spyOn(console, "error") .mockImplementation(() => {}); const content = JSON.stringify({ mcpServers: { valid: { command: "uvx", args: ["mcp-server"], }, invalid: { // Missing command, should default to stdio but fail validation args: ["--verbose"], }, }, }); const result = parser.parseContent(content); expect(result.success).toBe(false); expect(result.config?.mcpServers.valid).toBeDefined(); expect(result.config?.mcpServers.invalid).toBeUndefined(); consoleSpy.mockRestore(); }); it("should still fail completely in strict mode even with default type", () => { const parser = new MCPConfigParser({ strict: true }); const content = JSON.stringify({ mcpServers: { valid: { command: "uvx", args: ["mcp-server"], }, invalid: { // Missing command, should default to stdio but fail validation args: ["--verbose"], }, }, }); const result = parser.parseContent(content); expect(result.success).toBe(false); expect(result.config).toBeUndefined(); expect(result.validationErrors).toContain( 'Stdio server "invalid" must have a "command" string' ); }); it("should preserve env and other fields when type is defaulted", () => { const content = JSON.stringify({ mcpServers: { withEnv: { // Type omitted, should default to stdio command: "python", args: ["-m", "myserver"], env: { PYTHONPATH: "/custom/path", DEBUG: "true", }, }, }, }); const result = parser.parseContent(content); expect(result.success).toBe(true); expect(result.config?.mcpServers.withEnv).toMatchObject({ type: "stdio", command: "python", args: ["-m", "myserver"], env: { PYTHONPATH: "/custom/path", DEBUG: "true", }, }); }); it("should handle empty mcpServers object gracefully", () => { const content = JSON.stringify({ mcpServers: {}, }); const result = parser.parseContent(content); expect(result.success).toBe(true); expect(result.config).toEqual({ mcpServers: {}, }); }); it("should handle whitespace-only type values as empty", () => { const content = JSON.stringify({ mcpServers: { whitespaceType: { type: " ", command: "uvx", args: ["mcp-server"], }, }, }); const result = parser.parseContent(content); expect(result.success).toBe(true); expect(result.config?.mcpServers.whitespaceType).toMatchObject({ type: "stdio", command: "uvx", args: ["mcp-server"], env: {}, }); }); it("should handle multiple servers with various type field issues", () => { const parser = new MCPConfigParser({ strict: false }); const content = JSON.stringify({ mcpServers: { missing: { command: "uvx", args: ["mcp-1"], }, empty: { type: "", command: "uvx", args: ["mcp-2"], }, whitespace: { type: " \t ", command: "uvx", args: ["mcp-3"], }, nullType: { type: null, command: "uvx", args: ["mcp-4"], }, undefinedType: { type: undefined, command: "uvx", args: ["mcp-5"], }, explicit: { type: "stdio", command: "uvx", args: ["mcp-6"], }, invalidButMissingCommand: { // Should default to stdio but fail on missing command args: ["--help"], }, }, }); const result = parser.parseContent(content); expect(result.success).toBe(false); expect(Object.keys(result.config!.mcpServers)).toHaveLength(6); // All should default to stdio type expect(result.config?.mcpServers.missing?.type).toBe("stdio"); expect(result.config?.mcpServers.empty?.type).toBe("stdio"); expect(result.config?.mcpServers.whitespace?.type).toBe("stdio"); expect(result.config?.mcpServers.nullType?.type).toBe("stdio"); expect(result.config?.mcpServers.undefinedType?.type).toBe("stdio"); expect(result.config?.mcpServers.explicit?.type).toBe("stdio"); // The invalid one should not be included expect( result.config?.mcpServers.invalidButMissingCommand ).toBeUndefined(); // Should have validation error for the missing command expect(result.validationErrors).toContain( 'Stdio server "invalidButMissingCommand" must have a "command" string' ); }); }); });

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