Skip to main content
Glama

hypertool-mcp

manager.test.tsโ€ข13 kB
/** * Integration tests for Connection Manager */ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; import { ConnectionManager } from "./manager.js"; import { ConnectionFactory } from "./factory.js"; import { StdioServerConfig, HttpServerConfig } from "../types/config.js"; import { Logger } from "../utils/logging.js"; import { ConnectionState, ConnectionEventType, Connection, ConnectionEvent, } from "./types.js"; // Mock connection for testing class MockConnection implements Connection { public readonly id = "test-id"; public readonly serverName: string; public readonly config: any; public isConnectedState = false; public connectPromise: Promise<void> = Promise.resolve(); public disconnectPromise: Promise<void> = Promise.resolve(); public pingResult = true; private eventListeners = new Map<string, Function[]>(); constructor(serverName: string, config: any) { this.serverName = serverName; this.config = config; } get status() { return { state: this.isConnectedState ? ConnectionState.CONNECTED : ConnectionState.DISCONNECTED, serverId: this.id, serverName: this.serverName, retryCount: 0, transport: this.config.type, }; } get client() { return {}; } async connect(): Promise<void> { await this.connectPromise; this.isConnectedState = true; this.emit("connected", { type: "connected", serverId: this.id, serverName: this.serverName, timestamp: new Date(), }); } async disconnect(): Promise<void> { await this.disconnectPromise; this.isConnectedState = false; this.emit("disconnected", { type: "disconnected", serverId: this.id, serverName: this.serverName, timestamp: new Date(), }); } async ping(): Promise<boolean> { return this.pingResult; } isConnected(): boolean { return this.isConnectedState; } on(event: ConnectionEventType, callback: Function): void { if (!this.eventListeners.has(event)) { this.eventListeners.set(event, []); } this.eventListeners.get(event)!.push(callback); } off(event: ConnectionEventType, callback: Function): void { const listeners = this.eventListeners.get(event); if (listeners) { const index = listeners.indexOf(callback); if (index > -1) { listeners.splice(index, 1); } } } emit(event: ConnectionEventType, payload: Partial<ConnectionEvent>): void { const listeners = this.eventListeners.get(event); if (listeners) { listeners.forEach((listener) => listener(payload)); } } } // Mock connection factory class MockConnectionFactory extends ConnectionFactory { private connections = new Map<string, MockConnection>(); createConnection(serverName: string, config: any): Connection { const connection = new MockConnection(serverName, config); this.connections.set(serverName, connection); return connection; } getMockConnection(serverName: string): MockConnection | undefined { return this.connections.get(serverName); } } describe("ConnectionManager", () => { let manager: ConnectionManager; let mockFactory: MockConnectionFactory; const testServers = { "git-server": { type: "stdio" as const, command: "git-mcp-server", args: ["--stdio"], } as StdioServerConfig, "docker-server": { type: "http" as const, url: "http://localhost:3001/mcp", } as HttpServerConfig, }; beforeEach(() => { mockFactory = new MockConnectionFactory(); manager = new ConnectionManager({}, mockFactory); }); afterEach(async () => { try { await manager.stop(); } catch { // Ignore cleanup errors } }); describe("initialization", () => { it("should initialize with server configurations", async () => { await manager.initialize(testServers); expect(manager.getServerNames()).toEqual(["git-server", "docker-server"]); expect(manager.pool.size).toBe(2); }); it("should continue initializing when a server fails", async () => { const failingFactory = new MockConnectionFactory(); const originalCreate = failingFactory.createConnection.bind(failingFactory); vi.spyOn(failingFactory, "createConnection").mockImplementation( (serverName: string, config: any) => { if (serverName === "bad-server") { throw new Error("init failed"); } return originalCreate(serverName, config); } ); manager = new ConnectionManager({}, failingFactory); const errorSpy = vi .spyOn((manager as any)._logger, "error") .mockImplementation(() => {}); const servers = { "good-server": { type: "stdio" as const, command: "good" }, "bad-server": { type: "stdio" as const, command: "bad" }, }; await expect(manager.initialize(servers)).resolves.not.toThrow(); expect(manager.getServerNames()).toEqual(["good-server"]); expect(manager.pool.size).toBe(1); expect(errorSpy).toHaveBeenCalled(); expect(errorSpy.mock.calls[0][0]).toContain("bad-server"); errorSpy.mockRestore(); vi.restoreAllMocks(); }); it("should remove duplicate server names during initialization", async () => { const duplicateServers = { foo: { type: "stdio" as const, command: "one" }, FOO: { type: "stdio" as const, command: "two" }, bar: { type: "stdio" as const, command: "three" }, }; const errorSpy = vi .spyOn((manager as any)._logger, "error") .mockImplementation(() => {}); await manager.initialize(duplicateServers); expect(manager.getServerNames().sort()).toEqual(["bar", "foo"]); expect(manager.pool.size).toBe(2); expect(errorSpy).toHaveBeenCalled(); errorSpy.mockRestore(); }); it("should reject double initialization", async () => { await manager.initialize(testServers); await expect(manager.initialize(testServers)).rejects.toThrow( "Connection manager is already initialized" ); }); it("should validate server configurations", async () => { const invalidServers = { "invalid-server": { type: "invalid", } as any, }; await expect(manager.initialize(invalidServers)).rejects.toThrow(); }); }); describe("connection management", () => { beforeEach(async () => { await manager.initialize(testServers); }); it("should connect to a specific server", async () => { await manager.connect("git-server"); expect(manager.isServerConnected("git-server")).toBe(true); expect(manager.getConnectedServers()).toContain("git-server"); }); it("should disconnect from a specific server", async () => { await manager.connect("git-server"); await manager.disconnect("git-server"); expect(manager.isServerConnected("git-server")).toBe(false); }); it("should reconnect to a server", async () => { await manager.connect("git-server"); const mockConnection = mockFactory.getMockConnection("git-server")!; const disconnectSpy = vi.spyOn(mockConnection, "disconnect"); const connectSpy = vi.spyOn(mockConnection, "connect"); await manager.reconnect("git-server"); expect(disconnectSpy).toHaveBeenCalled(); expect(connectSpy).toHaveBeenCalled(); }); it("should handle connection to non-existent server", async () => { await expect(manager.connect("non-existent")).rejects.toThrow( 'Server "non-existent" not found in pool' ); }); }); describe("lifecycle management", () => { beforeEach(async () => { await manager.initialize(testServers); }); it("should start the connection manager", async () => { const startPromise = manager.start(); await startPromise; expect(manager.getConnectedServers().length).toBe(2); }); it("should stop the connection manager", async () => { await manager.start(); await manager.stop(); expect(manager.getConnectedServers().length).toBe(0); }); it("should handle multiple start calls gracefully", async () => { await manager.start(); await manager.start(); // Should not throw expect(manager.getConnectedServers().length).toBe(2); }); }); describe("server management", () => { beforeEach(async () => { await manager.initialize(testServers); }); it("should add a new server", async () => { const newServer: StdioServerConfig = { type: "stdio", command: "new-server", }; await manager.addServer("new-server", newServer); expect(manager.getServerNames()).toContain("new-server"); expect(manager.getServerConfig("new-server")).toEqual(newServer); }); it("should remove a server", async () => { await manager.removeServer("git-server"); expect(manager.getServerNames()).not.toContain("git-server"); expect(manager.pool.size).toBe(1); }); it("should reject duplicate server names", async () => { const duplicateServer: StdioServerConfig = { type: "stdio", command: "duplicate", }; await expect( manager.addServer("git-server", duplicateServer) ).rejects.toThrow( 'Server name conflict detected: "git-server" already exists' ); }); }); describe("event handling", () => { beforeEach(async () => { await manager.initialize(testServers); }); it("should forward connection events", async () => { const events: ConnectionEvent[] = []; manager.on("connected", (event) => { events.push(event); }); await manager.connect("git-server"); expect(events).toHaveLength(1); expect(events[0].type).toBe("connected"); expect(events[0].serverName).toBe("git-server"); }); it("should emit manager-specific events", async () => { const events: any[] = []; manager.on("started", (event) => { events.push(event); }); await manager.start(); expect(events).toHaveLength(1); expect(events[0].serverCount).toBe(2); }); }); describe("statistics", () => { beforeEach(async () => { await manager.initialize(testServers); }); it("should provide connection statistics", async () => { await manager.connect("git-server"); const stats = manager.getStats(); expect(stats.totalServers).toBe(2); expect(stats.connectedServers).toBe(1); expect(stats.disconnectedServers).toBe(1); expect(stats.connectionRate).toBe(0.5); expect(stats.activeConnections).toBe(1); expect(stats.poolSize).toBe(2); }); it("should handle empty connection pool", () => { const emptyManager = new ConnectionManager(); const stats = emptyManager.getStats(); expect(stats.totalServers).toBe(0); expect(stats.connectionRate).toBe(0); }); }); describe("error handling", () => { beforeEach(async () => { await manager.initialize(testServers); }); it("should handle connection failures gracefully", async () => { const mockConnection = mockFactory.getMockConnection("git-server")!; mockConnection.connectPromise = Promise.reject( new Error("Connection failed") ); await expect(manager.connect("git-server")).rejects.toThrow( "Connection failed" ); expect(manager.isServerConnected("git-server")).toBe(false); }); it("should handle operations before initialization", async () => { const uninitializedManager = new ConnectionManager(); await expect(uninitializedManager.connect("test")).rejects.toThrow( "Connection manager not initialized" ); }); }); describe("configuration validation", () => { it("should validate stdio server configuration", async () => { const validStdioServer = { "valid-stdio": { type: "stdio" as const, command: "test-command", args: ["--test"], env: { TEST: "value" }, }, }; await expect(manager.initialize(validStdioServer)).resolves.not.toThrow(); }); it("should validate HTTP server configuration", async () => { const validHttpServer = { "valid-http": { type: "http" as const, url: "http://localhost:3000/mcp", headers: { Authorization: "Bearer token" }, }, }; await expect(manager.initialize(validHttpServer)).resolves.not.toThrow(); }); it("should reject invalid configuration", async () => { const invalidServer = { invalid: null as any, }; await expect(manager.initialize(invalidServer)).rejects.toThrow( 'Invalid configuration for server "invalid"' ); }); }); });

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