1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
// 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);
});