Implement MCP Phase 1 infrastructure for agent tool integration with central hub, tool registry, and STDIO transport layers. Components: - McpHubService: Central registry for MCP server lifecycle - StdioTransport: STDIO process communication with JSON-RPC 2.0 - ToolRegistryService: Tool catalog management - McpController: REST API for MCP management Endpoints: - GET/POST /mcp/servers - List/register servers - POST /mcp/servers/:id/start|stop - Lifecycle control - DELETE /mcp/servers/:id - Unregister - GET /mcp/tools - List tools - POST /mcp/tools/:name/invoke - Invoke tool Features: - Full JSON-RPC 2.0 protocol support - Process lifecycle management - Buffered message parsing - Type-safe with no explicit any types - Proper cleanup on shutdown Tests: 85 passing with 90.9% coverage Fixes #132 Co-Authored-By: Claude Opus 4.5 <[email protected]>
219 lines
6.0 KiB
TypeScript
219 lines
6.0 KiB
TypeScript
import { describe, it, expect, beforeEach } from "vitest";
|
|
import { Test, TestingModule } from "@nestjs/testing";
|
|
import { ToolRegistryService } from "./tool-registry.service";
|
|
import type { McpTool } from "./interfaces";
|
|
|
|
describe("ToolRegistryService", () => {
|
|
let service: ToolRegistryService;
|
|
|
|
const mockTool1: McpTool = {
|
|
name: "test_tool_1",
|
|
description: "Test tool 1",
|
|
inputSchema: {
|
|
type: "object",
|
|
properties: {
|
|
param1: { type: "string" },
|
|
},
|
|
},
|
|
serverId: "server-1",
|
|
};
|
|
|
|
const mockTool2: McpTool = {
|
|
name: "test_tool_2",
|
|
description: "Test tool 2",
|
|
inputSchema: {
|
|
type: "object",
|
|
properties: {
|
|
param2: { type: "number" },
|
|
},
|
|
},
|
|
serverId: "server-1",
|
|
};
|
|
|
|
const mockTool3: McpTool = {
|
|
name: "test_tool_3",
|
|
description: "Test tool 3",
|
|
inputSchema: {
|
|
type: "object",
|
|
properties: {
|
|
param3: { type: "boolean" },
|
|
},
|
|
},
|
|
serverId: "server-2",
|
|
};
|
|
|
|
beforeEach(async () => {
|
|
const module: TestingModule = await Test.createTestingModule({
|
|
providers: [ToolRegistryService],
|
|
}).compile();
|
|
|
|
service = module.get<ToolRegistryService>(ToolRegistryService);
|
|
});
|
|
|
|
describe("initialization", () => {
|
|
it("should be defined", () => {
|
|
expect(service).toBeDefined();
|
|
});
|
|
|
|
it("should start with empty registry", () => {
|
|
const tools = service.listTools();
|
|
expect(tools).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
describe("registerTool", () => {
|
|
it("should register a new tool", () => {
|
|
service.registerTool(mockTool1);
|
|
const tool = service.getTool(mockTool1.name);
|
|
|
|
expect(tool).toBeDefined();
|
|
expect(tool?.name).toBe(mockTool1.name);
|
|
expect(tool?.description).toBe(mockTool1.description);
|
|
});
|
|
|
|
it("should update existing tool on re-registration", () => {
|
|
service.registerTool(mockTool1);
|
|
|
|
const updatedTool: McpTool = {
|
|
...mockTool1,
|
|
description: "Updated description",
|
|
};
|
|
|
|
service.registerTool(updatedTool);
|
|
const tool = service.getTool(mockTool1.name);
|
|
|
|
expect(tool?.description).toBe("Updated description");
|
|
});
|
|
|
|
it("should register multiple tools", () => {
|
|
service.registerTool(mockTool1);
|
|
service.registerTool(mockTool2);
|
|
service.registerTool(mockTool3);
|
|
|
|
const tools = service.listTools();
|
|
expect(tools).toHaveLength(3);
|
|
});
|
|
});
|
|
|
|
describe("unregisterTool", () => {
|
|
it("should remove a registered tool", () => {
|
|
service.registerTool(mockTool1);
|
|
service.unregisterTool(mockTool1.name);
|
|
|
|
const tool = service.getTool(mockTool1.name);
|
|
expect(tool).toBeUndefined();
|
|
});
|
|
|
|
it("should not throw error when unregistering non-existent tool", () => {
|
|
expect(() => service.unregisterTool("non-existent")).not.toThrow();
|
|
});
|
|
|
|
it("should only remove the specified tool", () => {
|
|
service.registerTool(mockTool1);
|
|
service.registerTool(mockTool2);
|
|
|
|
service.unregisterTool(mockTool1.name);
|
|
|
|
expect(service.getTool(mockTool1.name)).toBeUndefined();
|
|
expect(service.getTool(mockTool2.name)).toBeDefined();
|
|
});
|
|
});
|
|
|
|
describe("getTool", () => {
|
|
it("should return tool by name", () => {
|
|
service.registerTool(mockTool1);
|
|
const tool = service.getTool(mockTool1.name);
|
|
|
|
expect(tool).toEqual(mockTool1);
|
|
});
|
|
|
|
it("should return undefined for non-existent tool", () => {
|
|
const tool = service.getTool("non-existent");
|
|
expect(tool).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("listTools", () => {
|
|
it("should return all registered tools", () => {
|
|
service.registerTool(mockTool1);
|
|
service.registerTool(mockTool2);
|
|
service.registerTool(mockTool3);
|
|
|
|
const tools = service.listTools();
|
|
|
|
expect(tools).toHaveLength(3);
|
|
expect(tools).toContainEqual(mockTool1);
|
|
expect(tools).toContainEqual(mockTool2);
|
|
expect(tools).toContainEqual(mockTool3);
|
|
});
|
|
|
|
it("should return empty array when no tools registered", () => {
|
|
const tools = service.listTools();
|
|
expect(tools).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
describe("listToolsByServer", () => {
|
|
beforeEach(() => {
|
|
service.registerTool(mockTool1);
|
|
service.registerTool(mockTool2);
|
|
service.registerTool(mockTool3);
|
|
});
|
|
|
|
it("should return tools for specific server", () => {
|
|
const server1Tools = service.listToolsByServer("server-1");
|
|
|
|
expect(server1Tools).toHaveLength(2);
|
|
expect(server1Tools).toContainEqual(mockTool1);
|
|
expect(server1Tools).toContainEqual(mockTool2);
|
|
});
|
|
|
|
it("should return empty array for server with no tools", () => {
|
|
const tools = service.listToolsByServer("non-existent-server");
|
|
expect(tools).toHaveLength(0);
|
|
});
|
|
|
|
it("should not include tools from other servers", () => {
|
|
const server2Tools = service.listToolsByServer("server-2");
|
|
|
|
expect(server2Tools).toHaveLength(1);
|
|
expect(server2Tools).toContainEqual(mockTool3);
|
|
expect(server2Tools).not.toContainEqual(mockTool1);
|
|
});
|
|
});
|
|
|
|
describe("clearServerTools", () => {
|
|
beforeEach(() => {
|
|
service.registerTool(mockTool1);
|
|
service.registerTool(mockTool2);
|
|
service.registerTool(mockTool3);
|
|
});
|
|
|
|
it("should remove all tools for a server", () => {
|
|
service.clearServerTools("server-1");
|
|
|
|
const server1Tools = service.listToolsByServer("server-1");
|
|
expect(server1Tools).toHaveLength(0);
|
|
});
|
|
|
|
it("should not affect tools from other servers", () => {
|
|
service.clearServerTools("server-1");
|
|
|
|
const server2Tools = service.listToolsByServer("server-2");
|
|
expect(server2Tools).toHaveLength(1);
|
|
});
|
|
|
|
it("should not throw error for non-existent server", () => {
|
|
expect(() => service.clearServerTools("non-existent")).not.toThrow();
|
|
});
|
|
|
|
it("should allow re-registration after clearing", () => {
|
|
service.clearServerTools("server-1");
|
|
service.registerTool(mockTool1);
|
|
|
|
const tool = service.getTool(mockTool1.name);
|
|
expect(tool).toBeDefined();
|
|
});
|
|
});
|
|
});
|