import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { describe, expect, it } from "vitest"; import { createEngine } from "@nengine/core"; import { createEngineNamespaces, McpGateway } from "../src/index.js"; const TIME_YAML = [ "id: default", "dayLengthTicks: 480", "seasons:", " - { name: spring, dayCount: 28 }", "start: { day: 1, season: spring }", "", ].join("\n"); const FARM_YAML = [ "id: farm", "width: 2", "height: 2", 'tilesetRef: "tiles"', "spawn: { x: 0, y: 0 }", "layers:", " - name: ground", " data:", " - [a, b]", " - [b, a]", "", ].join("\n"); interface Setup { engine: ReturnType; gateway: McpGateway; tmp: string; close: () => Promise; } async function setup(): Promise { const tmp = await mkdtemp(join(tmpdir(), "farm-server-ns-")); await mkdir(join(tmp, "maps"), { recursive: true }); await writeFile(join(tmp, "time.yaml"), TIME_YAML); await writeFile(join(tmp, "maps", "farm.yaml"), FARM_YAML); const engine = createEngine({ contentDir: tmp }); const gateway = new McpGateway(); for (const namespace of createEngineNamespaces(engine, { gateway })) { gateway.register(namespace); } await engine.start(); return { engine, gateway, tmp, close: async () => { await engine.stop(); await rm(tmp, { recursive: true, force: true }); }, }; } async function callText(gateway: McpGateway, name: string, args: Record = {}): Promise<{ ok: boolean; text: string; }> { const result = await gateway.callTool(name, args); return { ok: result.isError !== true, text: result.content[0]?.text ?? "" }; } describe("server namespace subsystem toggles", () => { it("enable_subsystem/disable_subsystem reject non-string and empty names", async () => { const { gateway, close } = await setup(); try { const missing = await callText(gateway, "server.enable_subsystem", {}); expect(missing.ok).toBe(false); expect(missing.text).toMatch(/missing required arguments: name/); const empty = await callText(gateway, "server.disable_subsystem", { name: "" }); expect(empty.ok).toBe(false); expect(empty.text).toBe("server.disable_subsystem: name must be a non-empty string"); const nonString = await callText(gateway, "server.enable_subsystem", { name: 42 }); expect(nonString.ok).toBe(false); expect(nonString.text).toBe("server.enable_subsystem: name must be a non-empty string"); } finally { await close(); } }); it("enable_subsystem/disable_subsystem reject unknown subsystems", async () => { const { gateway, close } = await setup(); try { const enableUnknown = await callText(gateway, "server.enable_subsystem", { name: "nope" }); expect(enableUnknown.ok).toBe(false); expect(enableUnknown.text).toBe("server.enable_subsystem: unknown subsystem: nope"); const disableUnknown = await callText(gateway, "server.disable_subsystem", { name: "nope" }); expect(disableUnknown.ok).toBe(false); expect(disableUnknown.text).toBe("server.disable_subsystem: unknown subsystem: nope"); } finally { await close(); } }); it("disable removes a subsystem's tools/resources/prompts; enable restores them", async () => { const { gateway, close } = await setup(); try { const allTools = gateway.listTools().map((tool) => tool.name); expect(allTools).toContain("time.get"); expect(allTools).toContain("crop.create"); expect(allTools).toContain("server.info"); expect(gateway.listPrompts().map((prompt) => prompt.name)).toContain("crop.create-prompt"); const allResources = gateway.listResources().map((resource) => resource.uri); expect(allResources).toContain("time://state"); const disabled = await callText(gateway, "server.disable_subsystem", { name: "time" }); expect(disabled.ok).toBe(true); expect(JSON.parse(disabled.text)).toEqual({ name: "time", enabled: false }); const afterTools = gateway.listTools().map((tool) => tool.name); expect(afterTools).not.toContain("time.get"); expect(afterTools).not.toContain("time.advance"); expect(afterTools).toContain("server.info"); expect(gateway.listResources().map((resource) => resource.uri)).not.toContain("time://state"); const rejected = await callText(gateway, "time.get"); expect(rejected.ok).toBe(false); expect(rejected.text).toMatch(/namespace "time" is disabled/); const info = await callText(gateway, "server.info"); expect(info.ok).toBe(true); const subsystems = (JSON.parse(info.text) as { subsystems: { name: string; enabled: boolean }[] }).subsystems; expect(subsystems.find((s) => s.name === "time")).toEqual({ name: "time", enabled: false }); expect(subsystems.find((s) => s.name === "server")).toEqual({ name: "server", enabled: true }); const disabledCrop = await callText(gateway, "server.disable_subsystem", { name: "crop" }); expect(disabledCrop.ok).toBe(true); expect(gateway.listPrompts().map((prompt) => prompt.name)).not.toContain("crop.create-prompt"); const enabledCrop = await callText(gateway, "server.enable_subsystem", { name: "crop" }); expect(enabledCrop.ok).toBe(true); expect(gateway.listPrompts().map((prompt) => prompt.name)).toContain("crop.create-prompt"); const enabled = await callText(gateway, "server.enable_subsystem", { name: "time" }); expect(enabled.ok).toBe(true); expect(JSON.parse(enabled.text)).toEqual({ name: "time", enabled: true }); const restored = gateway.listTools().map((tool) => tool.name); expect(restored).toContain("time.get"); const get = await callText(gateway, "time.get"); expect(get.ok).toBe(true); expect(JSON.parse(get.text)).toEqual({ day: 1, season: "spring", tickInDay: 0 }); } finally { await close(); } }); it("disable_subsystem rejects disabling the server namespace itself (guard rail)", async () => { const { gateway, close } = await setup(); try { const guarded = await callText(gateway, "server.disable_subsystem", { name: "server" }); expect(guarded.ok).toBe(false); expect(guarded.text).toBe("server.disable_subsystem: cannot disable the server subsystem"); expect(gateway.listTools().map((tool) => tool.name)).toContain("server.info"); const status = gateway.status().find((s) => s.name === "server"); expect(status).toEqual({ name: "server", enabled: true }); } finally { await close(); } }); it("enable/disable state is per-gateway in-memory and does not touch engine systems", async () => { const { engine, gateway, close } = await setup(); try { await callText(gateway, "server.disable_subsystem", { name: "time" }); const timeSystem = engine.systems.get("time"); expect(timeSystem?.get()).toEqual({ day: 1, season: "spring", tickInDay: 0 }); expect(gateway.status().find((s) => s.name === "time")).toEqual({ name: "time", enabled: false }); } finally { await close(); } }); });