We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/jmagar/homelab-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server
// src/tools/scout.test.ts
import { beforeEach, describe, expect, it, vi } from "vitest";
import { DEFAULT_DOCKER_SOCKET } from "../constants.js";
import type { ServiceContainer } from "../services/container.js";
import { handleScoutTool } from "./scout.js";
describe("Scout Tool Handler", () => {
let mockContainer: ServiceContainer;
beforeEach(() => {
mockContainer = {
getSSHService: vi.fn(),
getFileService: vi.fn(),
getDockerService: vi.fn(),
getComposeService: vi.fn(),
getHostConfigRepository: vi.fn(() => ({
loadHosts: vi
.fn()
.mockReturnValue([{ name: "tootie", host: "tootie", protocol: "http", port: 2375 }]),
clearCache: vi.fn(),
})),
} as unknown as ServiceContainer;
});
describe("help system", () => {
it("should handle help action and return markdown by default", async () => {
const result = await handleScoutTool({ action: "help" }, mockContainer);
// Should contain simple actions
expect(result).toContain("nodes");
expect(result).toContain("peek");
expect(result).toContain("exec");
// Should contain nested actions with subactions
expect(result).toContain("zfs:pools");
expect(result).toContain("logs:syslog");
});
it("should handle help with topic filter for simple action", async () => {
const result = await handleScoutTool({ action: "help", topic: "nodes" }, mockContainer);
expect(result).toContain("nodes");
expect(result).not.toContain("peek");
expect(result).not.toContain("zfs");
});
it("should handle help with topic filter for nested action", async () => {
const result = await handleScoutTool({ action: "help", topic: "zfs:pools" }, mockContainer);
expect(result).toContain("zfs:pools");
expect(result).not.toContain("zfs:datasets");
});
it("should handle help with json format", async () => {
const result = await handleScoutTool({ action: "help", format: "json" }, mockContainer);
const parsed = JSON.parse(result);
expect(Array.isArray(parsed)).toBe(true);
expect(parsed.length).toBeGreaterThan(0);
// Should have both simple and nested actions
const actions = parsed.map((e: { action: string }) => e.action);
expect(actions).toContain("nodes");
expect(actions).toContain("zfs:pools");
});
it("should return empty help for non-existent topic", async () => {
const result = await handleScoutTool({ action: "help", topic: "nonexistent" }, mockContainer);
expect(result).toContain("No help available");
});
});
describe("routing", () => {
it("should route nodes action to simple handler", async () => {
// nodes action just lists configured hosts - override the default mock
const customMockContainer = {
...mockContainer,
getHostConfigRepository: vi.fn(() => ({
loadHosts: vi.fn().mockReturnValue([
{
name: "local",
host: "localhost",
protocol: "http",
port: 2375,
dockerSocketPath: DEFAULT_DOCKER_SOCKET,
},
]),
clearCache: vi.fn(),
})),
} as unknown as ServiceContainer;
const result = await handleScoutTool({ action: "nodes" }, customMockContainer);
// Should return the host list (from mock repository)
expect(result).toContain("local");
expect(result).toContain("localhost");
});
it("should route peek action to simple handler", async () => {
const mockFileService = {
readFile: vi.fn().mockResolvedValue({ content: "test", size: 4, truncated: false }),
};
(mockContainer.getFileService as ReturnType<typeof vi.fn>).mockReturnValue(mockFileService);
const result = await handleScoutTool(
{ action: "peek", target: "tootie:/etc/hosts" },
mockContainer
);
expect(mockFileService.readFile).toHaveBeenCalled();
expect(result).toBeDefined();
});
it("should route zfs action to zfs handler", async () => {
const mockSSHService = {
executeSSHCommand: vi
.fn()
.mockResolvedValue("NAME SIZE ALLOC FREE\ntank 10T 5T 5T"),
};
(mockContainer.getSSHService as ReturnType<typeof vi.fn>).mockReturnValue(mockSSHService);
const result = await handleScoutTool(
{ action: "zfs", subaction: "pools", host: "tootie" },
mockContainer
);
expect(mockSSHService.executeSSHCommand).toHaveBeenCalled();
expect(result).toBeDefined();
});
it("should route logs action to logs handler", async () => {
const mockSSHService = {
executeSSHCommand: vi
.fn()
.mockResolvedValue("Dec 15 10:00:00 tootie systemd[1]: Started service"),
};
(mockContainer.getSSHService as ReturnType<typeof vi.fn>).mockReturnValue(mockSSHService);
const result = await handleScoutTool(
{ action: "logs", subaction: "syslog", host: "tootie", lines: 100 },
mockContainer
);
expect(mockSSHService.executeSSHCommand).toHaveBeenCalled();
expect(result).toBeDefined();
});
});
describe("validation", () => {
it("should reject invalid action", async () => {
// NOTE: After A-M7 (Standardize validation patterns), Scout uses .parse() like Flux
// Zod throws ZodError with code "invalid_union" for discriminated union validation failures
await expect(handleScoutTool({ action: "invalid" }, mockContainer)).rejects.toThrow(
/invalid_union/i
);
});
it("should reject invalid target format for peek", async () => {
// NOTE: After A-M7 (Standardize validation patterns), Scout uses .parse() like Flux
// Zod includes the validation message in the error
await expect(
handleScoutTool({ action: "peek", target: "invalid" }, mockContainer)
).rejects.toThrow(/Must be 'hostname:\/path' format/);
});
});
});