import { resolve } from "node:path"; import { describe, expect, it } from "vitest"; import { createEngine } from "@nengine/core"; import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; import { createNengineMcpServer, createGateway } from "../src/server.js"; const repoRoot = resolve(process.cwd(), "../.."); const contentDir = resolve(repoRoot, "content"); const modsDir = resolve(repoRoot, "mods"); interface Roundtrip { engine: ReturnType; server: ReturnType; client: Client; close: () => Promise; } async function setup(gateway?: ReturnType): Promise { const engine = createEngine({ contentDir, modsDir }); await engine.start(); const server = gateway ? createNengineMcpServer(engine, gateway) : createNengineMcpServer(engine); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await server.connect(serverTransport); const client = new Client({ name: "roundtrip-test", version: "0.0.1" }); await client.connect(clientTransport); return { engine, server, client, close: async () => { await client.close(); await server.close(); await engine.stop(); }, }; } function textOf(result: { content: { type: string; text?: string }[] }): string { return result.content.find((block) => block.type === "text")?.text ?? ""; } function contentsOf(result: { contents: { text?: string }[] }): string { return result.contents[0]?.text ?? ""; } describe("apps/mcp-server SDK roundtrip (in-memory transport)", () => { it("initialize, listTools, callTool, resources, and prompts roundtrip", async () => { const { client, close } = await setup(); try { const tools = await client.listTools(); const toolNames = tools.tools.map((tool) => tool.name); expect(toolNames).toEqual( expect.arrayContaining([ "server.info", "server.status", "map.list", "map.get", "time.get", "time.advance", "time.set", ]), ); const result = await client.callTool({ name: "map.get", arguments: {} }); expect(result.isError).toBeUndefined(); const map = JSON.parse(textOf(result)) as { id: string; player: { x: number; y: number } }; expect(map.id).toBe("farm"); expect(map.player.x).toBe(2); const advanced = await client.callTool({ name: "time.advance", arguments: { ticks: 480 } }); const time = JSON.parse(textOf(advanced)) as { day: number }; expect(time.day).toBe(2); const resources = await client.listResources(); expect(resources.resources.map((resource) => resource.uri)).toEqual( expect.arrayContaining(["server://status", "time://state", "map://{mapId}"]), ); const state = await client.readResource({ uri: "time://state" }); expect(JSON.parse(contentsOf(state))).toEqual({ day: 2, season: "spring", tickInDay: 0 }); const prompts = await client.listPrompts(); expect(prompts.prompts.map((prompt) => prompt.name)).toContain("server.orient"); const orient = await client.getPrompt({ name: "server.orient", arguments: {} }); expect(orient.messages[0]?.content).toMatchObject({ type: "text", text: expect.stringContaining("nEngine MCP server") }); } finally { await close(); } }); it("disabled namespaces are absent from tools/list and fail on tools/call", async () => { const engine = createEngine({ contentDir, modsDir }); await engine.start(); const gateway = createGateway(engine); gateway.disable("time"); const server = createNengineMcpServer(engine, gateway); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await server.connect(serverTransport); const client = new Client({ name: "roundtrip-test", version: "0.0.1" }); await client.connect(clientTransport); try { const tools = await client.listTools(); expect(tools.tools.some((tool) => tool.name.startsWith("time."))).toBe(false); expect(tools.tools.some((tool) => tool.name === "map.get")).toBe(true); const rejected = await client.callTool({ name: "time.get", arguments: {} }); expect(rejected.isError).toBe(true); expect(textOf(rejected)).toMatch(/namespace "time" is disabled/); } finally { await client.close(); await server.close(); await engine.stop(); } }); it("surfaces resource errors over the wire", async () => { const { client, close } = await setup(); try { await expect(client.readResource({ uri: "map://does-not-exist" })).rejects.toThrow(/map not found/); } finally { await close(); } }); });