| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124 |
- 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<typeof createEngine>;
- server: ReturnType<typeof createNengineMcpServer>;
- client: Client;
- close: () => Promise<void>;
- }
- async function setup(gateway?: ReturnType<typeof createGateway>): Promise<Roundtrip> {
- 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();
- }
- });
- });
|