Files
edgeever/scripts/test-mcp.mjs
T

263 lines
9.7 KiB
JavaScript

import { strict as assert } from "node:assert";
import { globSync, readFileSync } from "node:fs";
import { Database } from "bun:sqlite";
import worker from "../apps/api/src/index.ts";
class SqliteD1PreparedStatement {
constructor(db, sql, bindings = []) {
this.db = db;
this.sql = sql;
this.bindings = bindings;
}
bind(...bindings) {
return new SqliteD1PreparedStatement(this.db, this.sql, bindings);
}
async all() {
return { results: this.db.query(this.sql).all(...this.bindings), success: true, meta: {} };
}
async first() {
return this.db.query(this.sql).get(...this.bindings) ?? null;
}
async run() {
this.db.query(this.sql).run(...this.bindings);
return { success: true, meta: {} };
}
}
class SqliteD1Database {
constructor(db) {
this.db = db;
}
prepare(sql) {
return new SqliteD1PreparedStatement(this.db, sql);
}
async batch(statements) {
return this.db.transaction(() =>
statements.map((statement) => this.db.query(statement.sql).run(...statement.bindings)))();
}
}
const sqlite = new Database(":memory:");
sqlite.exec("PRAGMA foreign_keys = ON");
for (const file of globSync("migrations/*.sql").sort()) {
sqlite.exec(readFileSync(file, "utf8"));
}
sqlite.run(
"INSERT INTO users (id, username, password_hash, display_name) VALUES (?, ?, ?, ?)",
["usr_zack", "zack42", "test", "Zack"],
);
sqlite.run(
"INSERT INTO workspace_members (workspace_id, user_id, role) VALUES (?, ?, ?)",
["ws_default", "usr_zack", "owner"],
);
sqlite.run(
"INSERT INTO notebooks (id, workspace_id, parent_id, name, sort_order) VALUES (?, ?, NULL, ?, ?)",
["nb_imports", "ws_default", "Imports", 100],
);
sqlite.run(
"INSERT INTO notebooks (id, workspace_id, parent_id, name, sort_order) VALUES (?, ?, ?, ?, ?)",
["nb_flomo", "ws_default", "nb_imports", "Flomo", 10],
);
sqlite.run(
"INSERT INTO users (id, username, password_hash, display_name) VALUES (?, ?, ?, ?)",
["usr_other", "other", "test", "Other"],
);
sqlite.run("INSERT INTO workspaces (id, name, is_personal) VALUES (?, ?, 1)", ["ws_other", "Other workspace"]);
sqlite.run(
"INSERT INTO workspace_members (workspace_id, user_id, role) VALUES (?, ?, ?)",
["ws_other", "usr_other", "member"],
);
sqlite.run(
"INSERT INTO notebooks (id, workspace_id, parent_id, name, sort_order) VALUES (?, ?, NULL, ?, ?)",
["nb_other", "ws_other", "Private", 10],
);
const token = "edgeever_mcp_test_token";
const tokenHash = new Bun.CryptoHasher("sha256").update(token).digest("hex");
sqlite.run(
"INSERT INTO api_tokens (id, workspace_id, name, token_hash, token_value, scopes_json) VALUES (?, ?, ?, ?, ?, ?)",
[
"tok_test",
"ws_default",
"MCP regression",
tokenHash,
token,
JSON.stringify(["read:notebooks", "read:memos", "write:memos", "read:resources", "read:tags"]),
],
);
const env = {
DB: new SqliteD1Database(sqlite),
RESOURCES: { delete: async () => undefined, get: async () => null, put: async () => undefined },
};
const executionContext = { waitUntil: () => undefined, passThroughOnException: () => undefined };
const fetchMcp = (payload, options = {}) =>
worker.fetch(
new Request("https://edgeever.test/mcp", {
method: options.method ?? "POST",
headers: {
...(options.auth === false ? {} : { Authorization: `Bearer ${token}` }),
Accept: "application/json, text/event-stream",
"Content-Type": "application/json",
...(options.headers ?? {}),
},
...(options.method === "GET" ? {} : { body: JSON.stringify(payload) }),
}),
env,
executionContext,
);
const rpc = (id, method, params) => ({ jsonrpc: "2.0", id, method, ...(params ? { params } : {}) });
const callTool = async (id, name, args = {}) => {
const response = await fetchMcp(rpc(id, "tools/call", { name, arguments: args }));
assert.equal(response.status, 200);
return response.json();
};
let response = await fetchMcp(rpc(1, "initialize", {
protocolVersion: "2025-11-25",
capabilities: {},
clientInfo: { name: "test", version: "1.0.0" },
}), { auth: false });
assert.equal(response.status, 401);
assert.match(response.headers.get("WWW-Authenticate") ?? "", /^Bearer /);
response = await fetchMcp(rpc(2, "initialize", {
protocolVersion: "2025-11-25",
capabilities: {},
clientInfo: { name: "test", version: "1.0.0" },
}));
assert.equal(response.status, 200);
let body = await response.json();
assert.equal(body.result.protocolVersion, "2025-11-25");
assert.equal(body.result.serverInfo.name, "edgeever");
assert.notEqual(body.result.serverInfo.version, "0.1.0");
response = await fetchMcp(rpc(3, "tools/list"));
body = await response.json();
const tools = new Map(body.result.tools.map((tool) => [tool.name, tool]));
for (const name of ["get_current_user", "get_notebook", "find_notebooks", "resolve_notebook_path"]) {
assert.ok(tools.has(name), `Missing MCP tool ${name}`);
assert.equal(tools.get(name).annotations.readOnlyHint, true);
assert.equal(tools.get(name).annotations.openWorldHint, false);
assert.equal(tools.get(name).outputSchema.type, "object");
}
assert.equal(tools.get("trash_memos").annotations.destructiveHint, true);
assert.equal(tools.get("create_memo").annotations.destructiveHint, false);
assert.equal(tools.get("import_memos").annotations.idempotentHint, true);
assert.equal(tools.get("import_memos").annotations.destructiveHint, false);
body = await callTool(4, "get_current_user");
assert.equal(body.result.structuredContent.user.username, "zack42");
assert.deepEqual(JSON.parse(body.result.content[0].text), body.result.structuredContent);
body = await callTool(5, "get_notebook", { notebookId: "nb_flomo" });
assert.equal(body.result.structuredContent.notebook.name, "Flomo");
body = await callTool(6, "find_notebooks", { name: "flo", parentId: "nb_imports" });
assert.deepEqual(body.result.structuredContent.notebooks.map((notebook) => notebook.id), ["nb_flomo"]);
body = await callTool(7, "resolve_notebook_path", { path: "Imports/Flomo" });
assert.equal(body.result.structuredContent.resolved, true);
assert.equal(body.result.structuredContent.notebook.id, "nb_flomo");
body = await callTool(8, "resolve_notebook_path", { path: "Imports/Missing" });
assert.equal(body.result.structuredContent.resolved, false);
assert.equal(body.result.structuredContent.failedSegment, "Missing");
body = await callTool(9, "get_notebook", { notebookId: "nb_other" });
assert.equal(body.result.isError, true);
assert.equal(body.result.structuredContent.error.code, "not_found");
body = await callTool(10, "list_notebooks");
assert.ok(!body.result.structuredContent.notebooks.some((notebook) => notebook.id === "nb_other"));
body = await callTool(11, "import_memos", {
source: "Flomo",
notebookId: "nb_flomo",
items: [
{
externalId: "flomo-1",
title: "Imported once",
contentMarkdown: "First import",
tags: ["migration"],
createdAt: "2026-07-01T00:00:00.000Z",
updatedAt: "2026-07-02T00:00:00.000Z",
},
{ externalId: "flomo-invalid", title: "Invalid", createdAt: "not-a-date" },
],
});
assert.equal(body.result.structuredContent.source, "flomo");
assert.equal(body.result.structuredContent.created, 1);
assert.equal(body.result.structuredContent.failed, 1);
assert.equal(body.result.structuredContent.results[0].status, "created");
const importedMemoId = body.result.structuredContent.results[0].memo.id;
body = await callTool(12, "import_memos", {
source: "flomo",
notebookId: "nb_flomo",
items: [{ externalId: "flomo-1", title: "A retry must not duplicate this memo" }],
});
assert.equal(body.result.structuredContent.created, 0);
assert.equal(body.result.structuredContent.skipped, 1);
assert.equal(body.result.structuredContent.results[0].memo.id, importedMemoId);
body = await callTool(13, "import_memos", {
source: "flomo",
notebookId: "nb_flomo",
dryRun: true,
items: [{ externalId: "flomo-2", title: "Preview only" }],
});
assert.equal(body.result.structuredContent.wouldCreate, 1);
assert.equal(body.result.structuredContent.results[0].status, "would_create");
assert.equal(sqlite.query("SELECT COUNT(*) AS count FROM memo_import_sources").get().count, 1);
assert.equal(sqlite.query("SELECT COUNT(*) AS count FROM memos WHERE id = ?").get(importedMemoId).count, 1);
sqlite.run(
`INSERT INTO resources (id, memo_id, bucket_name, object_key, kind, mime_type, filename, byte_size)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
["res_imported", importedMemoId, "test-resources", "mcp/imported.txt", "attachment", "text/plain", "imported.txt", 12],
);
body = await callTool(14, "list_memo_resources", { memoId: importedMemoId });
assert.equal(body.result.isError, false);
assert.deepEqual(body.result.structuredContent.resources.map((resource) => resource.id), ["res_imported"]);
response = await fetchMcp(rpc(15, "tools/call", { name: "unknown_tool", arguments: {} }));
assert.equal(response.status, 400);
body = await response.json();
assert.equal(body.error.code, -32602);
response = await fetchMcp([rpc(16, "tools/list")]);
assert.equal(response.status, 400);
response = await fetchMcp(rpc(17, "tools/list"), {
headers: { "MCP-Protocol-Version": "2099-01-01" },
});
assert.equal(response.status, 400);
response = await fetchMcp(rpc(18, "tools/list"), { headers: { Origin: "https://attacker.example" } });
assert.equal(response.status, 403);
response = await fetchMcp(rpc(19, "tools/list"), { headers: { Accept: "application/json" } });
assert.equal(response.status, 406);
response = await fetchMcp(rpc(20, "tools/list"), { headers: { "Content-Type": "text/plain" } });
assert.equal(response.status, 415);
response = await fetchMcp({ jsonrpc: "2.0", method: "notifications/initialized" });
assert.equal(response.status, 202);
response = await fetchMcp(null, { method: "GET" });
assert.equal(response.status, 405);
console.log("MCP protocol, identity, notebook lookup, and idempotent import regression passed");