Skip to main content
Glama

hypertool-mcp

mcp-integration.test.tsโ€ข22.7 kB
/** * Tests for PersonaMcpIntegration * * @fileoverview Comprehensive tests for MCP configuration integration functionality */ import { describe, it, expect, beforeEach, afterEach, vi, type MockedFunction, } from "vitest"; import { promises as fs } from "fs"; import { join, resolve } from "path"; import { PersonaMcpIntegration, createPersonaMcpIntegration, personaHasMcpConfig, validatePersonaMcpConfigFile, type McpConfigMergeOptions, type McpConfigMergeResult, } from "./mcp-integration.js"; import type { MCPConfig, ServerEntry } from "../types/config.js"; import type { PersonaAssets } from "./types.js"; import { PersonaErrorCode } from "./types.js"; // Mock fs promises vi.mock("fs", () => ({ promises: { access: vi.fn(), readFile: vi.fn(), }, })); // Mock MCPConfigParser vi.mock("../config/mcpConfigParser.js", () => ({ MCPConfigParser: vi.fn().mockImplementation(() => ({ parseFile: vi.fn(), })), })); const mockedFs = fs as any; import { MCPConfigParser } from "../config/mcpConfigParser.js"; const MockedMCPConfigParser = MCPConfigParser as any; describe("PersonaMcpIntegration", () => { let mockGetCurrentConfig: MockedFunction<() => Promise<MCPConfig | null>>; let mockSetCurrentConfig: MockedFunction< (config: MCPConfig) => Promise<void> >; let mockRestartConnections: MockedFunction<() => Promise<void>>; let integration: PersonaMcpIntegration; const sampleBaseConfig: MCPConfig = { mcpServers: { "base-server": { type: "stdio", command: "base-command", args: ["arg1"], env: { BASE_VAR: "base-value" }, }, }, }; const samplePersonaConfig: MCPConfig = { mcpServers: { "persona-server": { type: "http", url: "http://localhost:3000", headers: { Authorization: "Bearer token" }, }, "base-server": { type: "stdio", command: "persona-command", args: ["arg2"], env: { PERSONA_VAR: "persona-value" }, }, }, }; beforeEach(() => { vi.clearAllMocks(); mockGetCurrentConfig = vi.fn(); mockSetCurrentConfig = vi.fn(); mockRestartConnections = vi.fn(); // Setup default mock for MCPConfigParser MockedMCPConfigParser.mockImplementation(() => ({ parseFile: vi.fn().mockResolvedValue({ success: true, config: samplePersonaConfig, }), })); integration = new PersonaMcpIntegration( mockGetCurrentConfig, mockSetCurrentConfig, mockRestartConnections ); }); afterEach(() => { integration.dispose(); }); describe("constructor", () => { it("should create integration with default merge options", () => { expect(integration).toBeInstanceOf(PersonaMcpIntegration); expect(integration.hasBackup()).toBe(false); }); it("should accept custom merge options", () => { const customOptions: Partial<McpConfigMergeOptions> = { conflictResolution: "base-wins", preserveBaseEnv: false, }; const customIntegration = new PersonaMcpIntegration( mockGetCurrentConfig, mockSetCurrentConfig, mockRestartConnections, customOptions ); expect(customIntegration).toBeInstanceOf(PersonaMcpIntegration); customIntegration.dispose(); }); }); describe("createNullIntegration", () => { it("should create a null integration for personas without MCP configs", () => { const nullIntegration = PersonaMcpIntegration.createNullIntegration(); expect(nullIntegration).toBeInstanceOf(PersonaMcpIntegration); expect(nullIntegration.hasBackup()).toBe(false); nullIntegration.dispose(); }); }); describe("applyPersonaConfig", () => { const testConfigPath = "/test/persona/mcp.json"; it("should apply persona config when no base config exists", async () => { mockGetCurrentConfig.mockResolvedValue(null); const result = await integration.applyPersonaConfig(testConfigPath); expect(result.success).toBe(true); expect(result.mergedConfig).toEqual(samplePersonaConfig); expect(result.conflicts).toHaveLength(0); expect(result.stats.baseServersCount).toBe(0); expect(result.stats.personaServersCount).toBe(2); expect(result.stats.mergedServersCount).toBe(2); expect(mockSetCurrentConfig).toHaveBeenCalledWith(samplePersonaConfig); // When there's no base config, restartConnections should NOT be called // because setCurrentConfig already handles connecting to the servers expect(mockRestartConnections).not.toHaveBeenCalled(); }); it("should merge configs with persona-wins strategy", async () => { mockGetCurrentConfig.mockResolvedValue(sampleBaseConfig); const result = await integration.applyPersonaConfig(testConfigPath); expect(result.success).toBe(true); expect(result.conflicts).toHaveLength(1); expect(result.conflicts[0]).toContain("base-server"); expect(result.stats.conflictsResolved).toBe(1); // Check merged config has persona server priority const mergedConfig = result.mergedConfig!; expect(mergedConfig.mcpServers["base-server"]).toEqual( expect.objectContaining({ command: "persona-command", args: ["arg2"], }) ); // Should have both servers expect(Object.keys(mergedConfig.mcpServers)).toHaveLength(2); expect(mergedConfig.mcpServers["persona-server"]).toBeDefined(); }); it("should handle environment variable merging", async () => { mockGetCurrentConfig.mockResolvedValue(sampleBaseConfig); const result = await integration.applyPersonaConfig(testConfigPath, { conflictResolution: "persona-wins", mergeEnvironment: true, preserveBaseEnv: true, }); expect(result.success).toBe(true); // Check environment merging for conflicted server const mergedServer = result.mergedConfig!.mcpServers[ "base-server" ] as any; expect(mergedServer.env).toEqual({ BASE_VAR: "base-value", // Preserved from base PERSONA_VAR: "persona-value", // Added from persona }); }); it("should handle file system errors gracefully", async () => { // Mock parser to return file error MockedMCPConfigParser.mockImplementation(() => ({ parseFile: vi.fn().mockRejectedValue(new Error("File not found")), })); // Create new integration instance with the mocked parser integration.dispose(); integration = new PersonaMcpIntegration( mockGetCurrentConfig, mockSetCurrentConfig, mockRestartConnections ); const result = await integration.applyPersonaConfig(testConfigPath); expect(result.success).toBe(false); expect(result.errors).toHaveLength(1); expect(result.errors[0]).toContain("File system error"); expect(mockSetCurrentConfig).not.toHaveBeenCalled(); }); it("should handle invalid JSON gracefully", async () => { // Mock parser to return parse error MockedMCPConfigParser.mockImplementation(() => ({ parseFile: vi.fn().mockResolvedValue({ success: false, error: "Invalid JSON: Unexpected token", config: null, }), })); // Create new integration instance with the mocked parser integration.dispose(); integration = new PersonaMcpIntegration( mockGetCurrentConfig, mockSetCurrentConfig, mockRestartConnections ); const result = await integration.applyPersonaConfig(testConfigPath); expect(result.success).toBe(false); expect(result.errors).toHaveLength(1); expect(result.errors[0]).toContain("Failed to apply persona MCP config"); }); it("should create backup when merging with existing config", async () => { mockGetCurrentConfig.mockResolvedValue(sampleBaseConfig); const result = await integration.applyPersonaConfig(testConfigPath); expect(result.success).toBe(true); expect(integration.hasBackup()).toBe(true); const backup = integration.getBackupInfo(); expect(backup).toBeDefined(); expect(backup!.originalConfig).toEqual(sampleBaseConfig); expect(backup!.source).toBe("persona-integration"); }); it("should handle connection restart failures gracefully", async () => { // Set up with a base config to trigger the restart logic mockGetCurrentConfig.mockResolvedValue(sampleBaseConfig); mockRestartConnections.mockRejectedValue( new Error("Connection restart failed") ); // Mock file system access mockedFs.access.mockResolvedValue(undefined); mockedFs.readFile.mockResolvedValue(JSON.stringify(samplePersonaConfig)); // Make sure the parser mock is properly configured MockedMCPConfigParser.mockImplementation(() => ({ parseFile: vi.fn().mockResolvedValue({ success: true, config: samplePersonaConfig, }), })); const result = await integration.applyPersonaConfig(testConfigPath); expect(result.success).toBe(true); // Should still succeed expect(result.warnings).toContain( "Connection restart failed: Connection restart failed" ); }); }); describe("restoreOriginalConfig", () => { const testConfigPath = "/test/persona/mcp.json"; it("should restore original configuration", async () => { mockGetCurrentConfig.mockResolvedValue(sampleBaseConfig); // First apply a config to create backup await integration.applyPersonaConfig(testConfigPath); expect(integration.hasBackup()).toBe(true); // Clear the mock calls mockSetCurrentConfig.mockClear(); mockRestartConnections.mockClear(); // Restore the config await integration.restoreOriginalConfig(); expect(mockSetCurrentConfig).toHaveBeenCalledWith(sampleBaseConfig); expect(mockRestartConnections).toHaveBeenCalled(); expect(integration.hasBackup()).toBe(false); }); it("should handle missing backup gracefully", async () => { // No backup created expect(integration.hasBackup()).toBe(false); await expect( integration.restoreOriginalConfig() ).resolves.toBeUndefined(); expect(mockSetCurrentConfig).not.toHaveBeenCalled(); }); it("should handle restoration errors", async () => { mockGetCurrentConfig.mockResolvedValue(sampleBaseConfig); await integration.applyPersonaConfig(testConfigPath); mockSetCurrentConfig.mockRejectedValue( new Error("Config restore failed") ); await expect(integration.restoreOriginalConfig()).rejects.toThrow( "Failed to restore original MCP config" ); }); it("should handle connection restart errors during restore", async () => { mockGetCurrentConfig.mockResolvedValue(sampleBaseConfig); await integration.applyPersonaConfig(testConfigPath); mockRestartConnections.mockRejectedValue( new Error("Connection restart failed") ); // Should not throw, just log warning await expect( integration.restoreOriginalConfig() ).resolves.toBeUndefined(); expect(integration.hasBackup()).toBe(false); }); }); describe("configuration merging", () => { const testConfigPath = "/test/persona/mcp.json"; beforeEach(() => { mockedFs.access.mockResolvedValue(undefined); }); it("should handle base-wins conflict resolution", async () => { mockedFs.readFile.mockResolvedValue(JSON.stringify(samplePersonaConfig)); mockGetCurrentConfig.mockResolvedValue(sampleBaseConfig); const result = await integration.applyPersonaConfig(testConfigPath, { conflictResolution: "base-wins", }); expect(result.success).toBe(true); const mergedServer = result.mergedConfig!.mcpServers[ "base-server" ] as any; expect(mergedServer.command).toBe("base-command"); // Base wins expect(result.warnings).toContain( 'Using base config for server "base-server"' ); }); it("should handle user-choice conflict resolution", async () => { mockedFs.readFile.mockResolvedValue(JSON.stringify(samplePersonaConfig)); mockGetCurrentConfig.mockResolvedValue(sampleBaseConfig); const result = await integration.applyPersonaConfig(testConfigPath, { conflictResolution: "user-choice", }); expect(result.success).toBe(true); expect( result.warnings.some((w) => w.includes("Auto-resolved conflict")) ).toBe(true); }); it("should handle error conflict resolution", async () => { mockedFs.readFile.mockResolvedValue(JSON.stringify(samplePersonaConfig)); mockGetCurrentConfig.mockResolvedValue(sampleBaseConfig); const result = await integration.applyPersonaConfig(testConfigPath, { conflictResolution: "error", }); expect(result.success).toBe(false); expect(result.errors).toContain( 'Configuration conflict for server "base-server" - resolution required' ); }); it("should use custom conflict resolver", async () => { mockedFs.readFile.mockResolvedValue(JSON.stringify(samplePersonaConfig)); mockGetCurrentConfig.mockResolvedValue(sampleBaseConfig); const customResolver = vi.fn().mockReturnValue({ type: "stdio", command: "custom-command", env: {}, }); // Add missing mocks mockedFs.access.mockResolvedValue(undefined); const result = await integration.applyPersonaConfig(testConfigPath, { customResolver, }); expect(result.success).toBe(true); expect(customResolver).toHaveBeenCalledWith( "base-server", expect.any(Object), expect.any(Object) ); const mergedServer = result.mergedConfig!.mcpServers[ "base-server" ] as any; expect(mergedServer.command).toBe("custom-command"); }); }); describe("validateMcpConfig", () => { it("should validate correct MCP config structure", () => { expect(PersonaMcpIntegration.validateMcpConfig(sampleBaseConfig)).toBe( true ); }); it("should reject invalid config structures", () => { expect(PersonaMcpIntegration.validateMcpConfig(null)).toBe(false); expect(PersonaMcpIntegration.validateMcpConfig(undefined)).toBe(false); expect(PersonaMcpIntegration.validateMcpConfig("string")).toBe(false); expect(PersonaMcpIntegration.validateMcpConfig({})).toBe(false); expect( PersonaMcpIntegration.validateMcpConfig({ mcpServers: "invalid" }) ).toBe(false); }); }); }); describe("helper functions", () => { describe("personaHasMcpConfig", () => { it("should return true when persona has MCP config file", () => { const assets: PersonaAssets = { configFile: "/test/persona.yaml", mcpConfigFile: "/test/mcp.json", }; expect(personaHasMcpConfig(assets)).toBe(true); }); it("should return false when persona lacks MCP config file", () => { const assets: PersonaAssets = { configFile: "/test/persona.yaml", }; expect(personaHasMcpConfig(assets)).toBe(false); }); it("should return false when mcpConfigFile is undefined", () => { const assets: PersonaAssets = { configFile: "/test/persona.yaml", mcpConfigFile: undefined, }; expect(personaHasMcpConfig(assets)).toBe(false); }); }); describe("validatePersonaMcpConfigFile", () => { const testPath = "/test/mcp.json"; beforeEach(() => { vi.clearAllMocks(); }); it("should validate accessible and parseable MCP config file", async () => { mockedFs.access.mockResolvedValue(undefined); mockedFs.readFile.mockResolvedValue( JSON.stringify({ mcpServers: { "test-server": { type: "stdio", command: "test-command", }, }, }) ); const result = await validatePersonaMcpConfigFile(testPath); expect(result.isValid).toBe(true); expect(result.error).toBeUndefined(); }); it("should handle file access errors", async () => { mockedFs.access.mockRejectedValue(new Error("Permission denied")); const result = await validatePersonaMcpConfigFile(testPath); expect(result.isValid).toBe(false); expect(result.error).toBe("File access error: Permission denied"); }); it("should handle parsing errors", async () => { mockedFs.access.mockResolvedValue(undefined); mockedFs.readFile.mockResolvedValue("{ invalid json }"); // Mock parser to return parse error MockedMCPConfigParser.mockImplementation(() => ({ parseFile: vi.fn().mockResolvedValue({ success: false, error: "Invalid JSON syntax in config file", }), })); const result = await validatePersonaMcpConfigFile(testPath); expect(result.isValid).toBe(false); expect(result.error).toContain("Invalid JSON"); }); it("should handle validation errors", async () => { mockedFs.access.mockResolvedValue(undefined); mockedFs.readFile.mockResolvedValue( JSON.stringify({ // Missing mcpServers field servers: {}, }) ); // Mock parser to return validation error MockedMCPConfigParser.mockImplementation(() => ({ parseFile: vi.fn().mockResolvedValue({ success: false, validationErrors: ["Config missing 'mcpServers' field"], }), })); const result = await validatePersonaMcpConfigFile(testPath); expect(result.isValid).toBe(false); expect(result.error).toContain("missing 'mcpServers' field"); }); }); describe("createPersonaMcpIntegration", () => { it("should create integration with provided handlers", () => { const getCurrentConfig = vi.fn(); const setCurrentConfig = vi.fn(); const restartConnections = vi.fn(); const integration = createPersonaMcpIntegration( getCurrentConfig, setCurrentConfig, restartConnections ); expect(integration).toBeInstanceOf(PersonaMcpIntegration); integration.dispose(); }); it("should create integration with merge options", () => { const getCurrentConfig = vi.fn(); const setCurrentConfig = vi.fn(); const mergeOptions: Partial<McpConfigMergeOptions> = { conflictResolution: "base-wins", }; const integration = createPersonaMcpIntegration( getCurrentConfig, setCurrentConfig, undefined, mergeOptions ); expect(integration).toBeInstanceOf(PersonaMcpIntegration); integration.dispose(); }); }); }); describe("merge scenarios", () => { let mockGetCurrentConfig: MockedFunction<() => Promise<MCPConfig | null>>; let mockSetCurrentConfig: MockedFunction< (config: MCPConfig) => Promise<void> >; let integration: PersonaMcpIntegration; beforeEach(() => { vi.clearAllMocks(); mockGetCurrentConfig = vi.fn(); mockSetCurrentConfig = vi.fn(); mockedFs.access.mockResolvedValue(undefined); // Setup MCPConfigParser mock for complex persona config MockedMCPConfigParser.mockImplementation(() => ({ parseFile: vi.fn().mockImplementation((filePath: string) => { if (filePath === "/test/mcp.json") { const complexPersona: MCPConfig = { mcpServers: { "server-a": { type: "stdio", command: "persona-a", env: { PERSONA_A: "persona-value-a" }, }, "server-d": { type: "http", url: "http://persona-d.com", }, }, }; return Promise.resolve({ success: true, config: complexPersona, }); } return Promise.resolve({ success: true, config: samplePersonaConfig, }); }), })); integration = new PersonaMcpIntegration( mockGetCurrentConfig, mockSetCurrentConfig ); }); afterEach(() => { integration.dispose(); }); it("should handle complex merging scenario with multiple conflicts", async () => { const complexBase: MCPConfig = { mcpServers: { "server-a": { type: "stdio", command: "base-a", env: { BASE_A: "value-a" }, }, "server-b": { type: "http", url: "http://base-b.com", headers: { "X-Base": "base-b" }, }, "server-c": { type: "sse", url: "http://base-c.com", }, }, }; const complexPersona: MCPConfig = { mcpServers: { "server-a": { type: "stdio", command: "persona-a", env: { PERSONA_A: "persona-value-a" }, }, "server-d": { type: "http", url: "http://persona-d.com", }, }, }; mockGetCurrentConfig.mockResolvedValue(complexBase); mockedFs.access.mockResolvedValue(undefined); mockedFs.readFile.mockResolvedValue(JSON.stringify(complexPersona)); const result = await integration.applyPersonaConfig("/test/mcp.json", { conflictResolution: "persona-wins", mergeEnvironment: true, preserveBaseEnv: true, }); expect(result.success).toBe(true); expect(result.conflicts).toHaveLength(1); // Only server-a conflicts expect(result.stats.baseServersCount).toBe(3); expect(result.stats.personaServersCount).toBe(2); expect(result.stats.mergedServersCount).toBe(4); // server-a, server-b, server-c, server-d const merged = result.mergedConfig!; // Check merged server-a (conflict resolved with persona-wins + env merge) const serverA = merged.mcpServers["server-a"] as any; expect(serverA.command).toBe("persona-a"); // Persona wins expect(serverA.env).toEqual({ BASE_A: "value-a", // Preserved from base PERSONA_A: "persona-value-a", // Added from persona }); // Check preserved base servers expect(merged.mcpServers["server-b"]).toEqual( complexBase.mcpServers["server-b"] ); expect(merged.mcpServers["server-c"]).toEqual( complexBase.mcpServers["server-c"] ); // Check added persona server expect(merged.mcpServers["server-d"]).toEqual( complexPersona.mcpServers["server-d"] ); }); });

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