/** * End-to-end smoke test: launch the built server over stdio with a real MCP * client, list tools, read the UI resource, and exercise open_markdown + * submit_batch. No host/webview needed — this verifies the server contract. */ import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; import { writeFile, mkdir, readFile, rm } from "node:fs/promises"; import path from "node:path"; import { fileURLToPath } from "node:url"; const here = path.dirname(fileURLToPath(import.meta.url)); const root = path.join(here, ".."); const sample = path.join(here, "sample.md"); let failures = 0; const ok = (c, m) => { console.log(`${c ? "PASS" : "FAIL"} ${m}`); if (!c) failures++; }; await writeFile(sample, "# Title\n\nFirst paragraph with **bold**.\n\n- item one\n- item two\n"); const transport = new StdioClientTransport({ command: process.execPath, args: [path.join(root, "dist", "index.js"), "--stdio"], }); const client = new Client({ name: "smoke", version: "0.0.0" }); await client.connect(transport); // 1. tools/list — model-facing visibility const { tools } = await client.listTools(); const names = tools.map((t) => t.name).sort(); ok(names.includes("open_markdown"), "open_markdown tool registered"); ok(names.includes("submit_batch"), "submit_batch tool registered"); const openTool = tools.find((t) => t.name === "open_markdown"); ok( JSON.stringify(openTool?._meta ?? {}).includes("ui://markdown-review/mcp-app.html"), "open_markdown carries ui.resourceUri meta", ); // 2. resources — the UI html is served const { resources } = await client.listResources(); ok( resources.some((r) => r.uri === "ui://markdown-review/mcp-app.html"), "UI resource listed", ); const res = await client.readResource({ uri: "ui://markdown-review/mcp-app.html" }); ok( (res.contents?.[0]?.text ?? "").includes("Markdown Review"), "UI resource returns the app HTML", ); // 3. open_markdown returns the file content + empty comment set const opened = await client.callTool({ name: "open_markdown", arguments: { path: sample, mode: "edit" }, }); const sc = opened.structuredContent ?? {}; ok(sc.markdown?.includes("First paragraph"), "open_markdown returns file content"); ok(Array.isArray(sc.existingComments) && sc.existingComments.length === 0, "no pre-existing comments"); ok(sc.mode === "edit", "mode echoed back"); // 4. submit_batch persists to the sidecar, then open_markdown reads it back const comment = { id: "c-test-1", anchor: { line_start: 3, line_end: 3, char_start: 9, char_end: 24, quote: "First paragraph" }, body: "Tighten this sentence.", store: "sidecar", created: new Date().toISOString(), }; const saved = await client.callTool({ name: "submit_batch", arguments: { file: sc.path, comments: [comment], mode: "edit" }, }); ok(saved.structuredContent?.saved === true, "submit_batch reports saved"); ok(saved.structuredContent?.count === 1, "submit_batch count == 1"); const reopened = await client.callTool({ name: "open_markdown", arguments: { path: sample, mode: "review" }, }); const round = reopened.structuredContent?.existingComments ?? []; ok(round.length === 1 && round[0].body === "Tighten this sentence.", "sidecar comment round-trips on reopen"); await client.close(); // cleanup await rm(sample, { force: true }); await rm(path.join(here, ".claude"), { recursive: true, force: true }); console.log(`\n${failures === 0 ? "ALL PASSED" : failures + " FAILED"}`); process.exit(failures === 0 ? 0 : 1);