// src/tools/index.test.ts
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { getSchemaDescription } from "@modelcontextprotocol/sdk/server/zod-compat.js";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { FluxSchema } from "../schemas/flux/index.js";
import { ScoutSchema } from "../schemas/scout/index.js";
import type { ServiceContainer } from "../services/container.js";
import { logError } from "../utils/errors.js";
import { handleFluxTool } from "./flux.js";
import { registerTools } from "./index.js";
import { handleScoutTool } from "./scout.js";
vi.mock("./flux.js", () => ({
handleFluxTool: vi.fn(),
}));
vi.mock("./scout.js", () => ({
handleScoutTool: vi.fn(),
}));
vi.mock("../utils/errors.js", () => ({
logError: vi.fn(),
sanitizeParams: vi.fn((params) => params), // Pass-through mock for testing
}));
describe("Tool Registration", () => {
beforeEach(() => {
vi.mocked(handleFluxTool).mockReset();
vi.mocked(handleScoutTool).mockReset();
vi.mocked(logError).mockReset();
});
it("should register flux and scout tools", () => {
const server = {
registerTool: vi.fn(),
} as McpServer;
const container = {} as ServiceContainer;
registerTools(server, container);
// Verify registerTool was called (not addTool)
expect(server.registerTool).toHaveBeenCalledTimes(2);
// Check first call (flux)
const mockFn = server.registerTool as ReturnType<typeof vi.fn>;
const fluxCall = mockFn.mock.calls[0] as unknown[];
expect(fluxCall[0]).toBe("flux");
expect(fluxCall[1]).toMatchObject({
title: "Flux Tool",
description: expect.stringContaining("Docker"),
inputSchema: expect.any(Object),
});
// Check second call (scout)
const scoutCall = mockFn.mock.calls[1] as unknown[];
expect(scoutCall[0]).toBe("scout");
expect(scoutCall[1]).toMatchObject({
title: "Scout Tool",
description: expect.stringContaining("SSH"),
inputSchema: expect.any(Object),
});
});
it("should throw if container is not provided", () => {
const server = {
registerTool: vi.fn(),
} as McpServer;
expect(() => registerTools(server)).toThrow("ServiceContainer is required");
});
it("should log and rethrow errors from the flux handler", async () => {
const server = {
registerTool: vi.fn(),
} as McpServer;
const container = {
constructor: { name: "ServiceContainer" },
} as ServiceContainer;
registerTools(server, container);
const mockFn = server.registerTool as ReturnType<typeof vi.fn>;
const fluxHandler = mockFn.mock.calls[0]?.[2] as (params: unknown) => Promise<unknown>;
const error = new Error("flux failure");
vi.mocked(handleFluxTool).mockRejectedValueOnce(error);
await expect(fluxHandler({ host: "alpha" })).rejects.toThrow(error);
expect(logError).toHaveBeenCalledWith(error, {
operation: "flux:handler",
metadata: {
message: "Flux Tool execution failed", // Generated from tool.title in registry
params: { host: "alpha" },
container: { type: "ServiceContainer" },
},
});
});
it("should log and rethrow errors from the scout handler", async () => {
const server = {
registerTool: vi.fn(),
} as McpServer;
const container = {
constructor: { name: "ServiceContainer" },
} as ServiceContainer;
registerTools(server, container);
const mockFn = server.registerTool as ReturnType<typeof vi.fn>;
const scoutHandler = mockFn.mock.calls[1]?.[2] as (params: unknown) => Promise<unknown>;
const error = new Error("scout failure");
vi.mocked(handleScoutTool).mockRejectedValueOnce(error);
await expect(scoutHandler({ host: "beta" })).rejects.toThrow(error);
expect(logError).toHaveBeenCalledWith(error, {
operation: "scout:handler",
metadata: {
message: "Scout Tool execution failed", // Generated from tool.title in registry
params: { host: "beta" },
container: { type: "ServiceContainer" },
},
});
});
it("should extract descriptions from schemas", () => {
// Test that tool descriptions include the schema description
const server = { registerTool: vi.fn() } as McpServer;
const container = {} as ServiceContainer;
registerTools(server, container);
const mockFn = server.registerTool as ReturnType<typeof vi.fn>;
const fluxCall = mockFn.mock.calls[0] as unknown[];
const fluxConfig = fluxCall[1] as { description: string };
expect(fluxConfig.description).toContain(getSchemaDescription(FluxSchema));
const scoutCall = mockFn.mock.calls[1] as unknown[];
const scoutConfig = scoutCall[1] as { description: string };
expect(scoutConfig.description).toContain(getSchemaDescription(ScoutSchema));
});
it("should not use fallback descriptions", () => {
// Ensure .describe() was actually added with meaningful content
const fluxDesc = getSchemaDescription(FluxSchema);
const scoutDesc = getSchemaDescription(ScoutSchema);
// Verify descriptions are truthy and have actual content
expect(fluxDesc).toBeTruthy();
expect(scoutDesc).toBeTruthy();
expect(fluxDesc?.length).toBeGreaterThan(0);
expect(scoutDesc?.length).toBeGreaterThan(0);
// Verify they contain expected keywords
expect(fluxDesc).toContain("Docker");
expect(scoutDesc).toContain("SSH");
});
});