Antigravity-style inline markdown commenting as an installable Claude plugin. - MCP Apps webview (markdown-it renderer + comment UI) - three lanes: Answer Now (sendMessage), Add to batch, Submit All (persist+send) - edit/review mode toggle; sidecar comment persistence - self-contained prebuilt server/dist (zero runtime node_modules) - self-marketplace manifest for /plugin install - 11/11 stdio smoke tests passing Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
88 lines
3.5 KiB
JavaScript
88 lines
3.5 KiB
JavaScript
/**
|
|
* 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);
|