~ knotless Search/
↑↓ move↵ open⇧↵ all results

johnandersen777.bsky.social/atlibs

0
0
main
100 lines · 7.8 KB Raw History
// node --test javascript/src/mcp/mcp.test.mjs   (after `cargo wasm`): module actions as MCP tools, on the
// compose example's lookup (it doubles n, calling a progress capability first when given one).
import assert from "node:assert/strict";
import { mkdtemp, readFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { test } from "node:test";

import { createNodeContext } from "../server/index.js";
import { actionsServer, argumentsOf, exposed, hostRun, ingressMcp, inputSchema } from "./index.js";

const wasm = new Uint8Array(await readFile(new URL("../../../target/wasm32-unknown-unknown/release/compose.wasm", import.meta.url)));
const lookup = { entry: "lookup", label: "Lookup", description: "Doubles n.", readOnly: true, fields: [{ key: "n", label: "n", type: "integer", required: true, description: "the number to double" }] };
const write = { entry: "lookup", label: "Lookup (as a write)", fields: [{ key: "n", type: "integer" }] };
const mod = (name, actions, extra) => ({ module: wasm, name, id: `test:${name}`, calls: [], actions, ...extra });
const rpc = (server, method, params, id = 1, options) => server.message({ jsonrpc: "2.0", id, method, params }, options);
const ctx = createNodeContext({ id: "mcp-test", stateDir: await mkdtemp(join(tmpdir(), "atlibs-mcp-")) });
test.after(() => ctx.close());

test("fields become a JSON Schema; arguments are coerced to it, undeclared ones dropped", () => {
	const fields = [{ key: "n", type: "integer", required: true }, { key: "x", type: "number" }, { key: "on", type: "boolean" }, { key: "tags", type: "array" }, { key: "o", type: "object" }, { key: "s", label: "S" }];
	assert.deepEqual(inputSchema(fields).required, ["n"]);
	assert.deepEqual(inputSchema(fields).properties.s, { type: "string", title: "S", description: "S" });
	assert.equal(inputSchema(fields).additionalProperties, false);
	assert.throws(() => inputSchema([{ key: "k", type: "date" }]), /type date is not one of/);
	assert.deepEqual(argumentsOf(fields, { n: "4", x: "1.5", on: "false", tags: '["a"]', o: { a: 1 }, s: 7, did: "did:plc:mallory" }), { args: { n: 4, x: 1.5, on: false, tags: ["a"], o: { a: 1 }, s: "7" } });
	assert.deepEqual(argumentsOf(fields, { n: 1.5, on: "yes", tags: "{}", s: {} }), { problems: ["n must be an integer", "on must be a boolean", "tags must be an array", "s must be a string"] });
	assert.deepEqual(argumentsOf(fields, { n: "" }), { problems: ["n is required"] });
});

test("tools from actions; a read runs with no confirm, a write asks first; host options per module", async () => {
	const asked = [];
	const server = actionsServer({
		name: "compose",
		modules: [mod("compose", [lookup]), mod("compose-w", [write])],
		ctx,
		host: { "compose-w": { input: { n: 50 }, confirm: async (q) => (asked.push(q), q.arguments.n === 2) } },
	});
	const { tools } = (await rpc(server, "tools/list")).result;
	assert.deepEqual(tools.map((t) => [t.name, t.annotations.readOnlyHint, t.inputSchema.properties.n.type]), [["lookup", true, "integer"], ["w_lookup", false, "integer"]]);
	assert.equal(server.readOnly("lookup"), true);
	assert.equal(server.readOnly("w_lookup"), false);

	const read = await rpc(server, "tools/call", { name: "lookup", arguments: { n: "21", did: "did:plc:mallory" } });
	assert.deepEqual(read.result.structuredContent, { doubled: 42, ack: null, caller: null });
	const bad = await rpc(server, "tools/call", { name: "lookup", arguments: {} });
	assert.equal(bad.result.isError, true);
	assert.match(bad.result.content[0].text, /n is required/);

	// the module's own confirm; host input wins over the arguments
	assert.match((await rpc(server, "tools/call", { name: "w_lookup", arguments: { n: 3 } })).result.content[0].text, /declined/);
	assert.equal((await rpc(server, "tools/call", { name: "w_lookup", arguments: { n: 2 } })).result.structuredContent.doubled, 100);
	assert.deepEqual(asked.map((q) => [q.tool, q.module, q.arguments.n]), [["w_lookup", "compose-w", 3], ["w_lookup", "compose-w", 2]]);

	assert.equal((await rpc(server, "tools/call", { name: "nope" })).error.code, -32602);
	assert.equal((await rpc(server, "resources/list")).error.code, -32601);
	assert.equal(await server.message({ jsonrpc: "2.0", method: "notifications/initialized" }), null);
});

test("progress: a module's progress capability, as MCP notifications/progress when the transport carries them", async () => {
	const server = actionsServer({ name: "compose", modules: [mod("compose", [lookup])], ctx, host: { compose: { progress: true } } });
	const sent = [];
	const notify = (n) => sent.push(n);
	const out = await rpc(server, "tools/call", { name: "lookup", arguments: { n: 1 }, _meta: { progressToken: "p1" } }, 1, { notify });
	assert.equal(out.result.structuredContent.doubled, 2);
	assert.deepEqual(sent, [{ jsonrpc: "2.0", method: "notifications/progress", params: { progressToken: "p1", progress: 1, message: '{"step":1}' } }]);
	// no notify (a composition handler, ingressMcp): the module gets no capability
	assert.equal((await rpc(server, "tools/call", { name: "lookup", arguments: { n: 1 }, _meta: { progressToken: "p1" } })).result.structuredContent.ack, null);

	// hostRun alone: the per-call callback
	const seen = [];
	const ran = await hostRun(ctx, { compose: { progress: true } })(mod("compose", [lookup]), "lookup", { n: 4 }, { progress: (v) => (seen.push(v), { ok: true }) });
	assert.deepEqual([ran.result, seen], [{ doubled: 8, ack: { ok: true }, caller: null }, [{ step: 1 }]]);
});

test("transports and attest reach only a module that lists its calls", async () => {
	// the compose example's caller calls `nsid` and returns its outcome
	const run = hostRun(ctx, () => ({ call: async (nsid) => ({ via: `session ${nsid}` }), calls: { "com.example.t.mcp": async () => ({ via: "transport" }) }, attest: async () => ({ via: "attest" }) }));
	const outcome = async (calls, nsid) => (await run(mod("t", [], { calls }), "caller", { nsid })).result.q;
	assert.deepEqual(await outcome(["com.example.t.mcp"], "com.example.t.mcp"), { ok: true, output: { via: "transport" } });
	assert.deepEqual(await outcome(["com.fedfork.exec.attest"], "com.fedfork.exec.attest"), { ok: true, output: { via: "attest" } });
	// lists no calls (may call anything): the session's call, never the host's own
	assert.deepEqual(await outcome(undefined, "com.example.t.mcp"), { ok: true, output: { via: "session com.example.t.mcp" } });
});

test("exposed and ingressMcp: a public view over stateless Streamable HTTP, with CORS", async () => {
	const server = actionsServer({ name: "compose", modules: [mod("compose", [lookup]), mod("compose-w", [write])], ctx, confirm: async () => true });
	const forward = ingressMcp(() => ({ compose: exposed(server.message, server.readOnly) }));
	const post = (body) => forward({ method: "POST", path: "/mcp/compose", body: JSON.stringify(body), callerDid: "did:plc:remote" });
	assert.deepEqual((await post({ jsonrpc: "2.0", id: 1, method: "tools/list" })).body.result.tools.map((t) => t.name), ["lookup"]);
	assert.match((await post({ jsonrpc: "2.0", id: 2, method: "tools/call", params: { name: "w_lookup", arguments: { n: 1 } } })).body.error.message, /Unknown tool/);
	const called = await post({ jsonrpc: "2.0", id: 3, method: "tools/call", params: { name: "lookup", arguments: { n: 5 } } });
	assert.equal(called.body.result.structuredContent.doubled, 10);
	assert.deepEqual(called.headers, { "access-control-allow-origin": "*", "access-control-expose-headers": "mcp-session-id" });
	assert.equal((await post({ jsonrpc: "2.0", method: "notifications/initialized" })).status, 202);
	assert.equal((await forward({ method: "GET", path: "/mcp/compose" })).status, 405);
	assert.equal((await forward({ method: "OPTIONS", path: "/mcp/compose" })).status, 204);
	assert.deepEqual((await forward({ method: "POST", path: "/" })).body.servers, ["/mcp/compose"]);
	assert.equal((await forward({ method: "POST", path: "/mcp/compose", body: "[" })).body.error.code, -32700);
});