| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183 |
- 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<typeof createEngine>;
- gateway: McpGateway;
- tmp: string;
- close: () => Promise<void>;
- }
- async function setup(): Promise<Setup> {
- 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<string, unknown> = {}): 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();
- }
- });
- });
|