char-slop/berry

multipurpose self-modifiable agentic harness

git clone https://git.t4t.associates/char-slop/berry

Charlotte Somupdate to pi-mcp 1.2.0c3bbb3a

main
13.5 KiB368 linesraw
1import {
2  assert,
3  assertEquals,
4  assertNotEquals,
5  assertRejects,
6  assertStringIncludes,
7} from "@std/assert";
8import { rootedEnvironment, testExtension } from "@berry/api/testing.ts";
9import type { Environment, JsonValue, Process } from "@berry/api";
10import { tempDir, until } from "@berry/util";
11import mcp from "./mod.ts";
12
13type Result = {
14  content: { type: string; text: string }[];
15  isError: boolean;
16  structuredContent: {
17    pid: number;
18    count: number;
19    cwd: string;
20    token: string;
21    cancelled: number;
22    arguments: Record<string, JsonValue>;
23    roots: { uri: string }[];
24  };
25};
26
27async function within<T>(promise: Promise<T>, ms = 5000): Promise<T> {
28  let timer: ReturnType<typeof setTimeout> | undefined;
29  try {
30    return await Promise.race([
31      promise,
32      new Promise<never>((_, reject) =>
33        timer = setTimeout(() => reject(new Error("operation did not finish")), ms)
34      ),
35    ]);
36  } finally {
37    clearTimeout(timer);
38  }
39}
40
41async function fixture() {
42  const dir = await tempDir({ prefix: "mcp-test-" });
43  await Deno.mkdir(dir.path + "/project");
44  await Deno.mkdir(dir.path + "/other");
45  await Deno.writeTextFile(
46    dir.path + "/server.ts",
47    await Deno.readTextFile(new URL("./test-server.ts", import.meta.url)),
48  );
49  const env = rootedEnvironment(dir.path, {
50    cwd: "/project",
51    variables: { MCP_TOKEN: "remote-secret" },
52  });
53  const spawn = env.spawn;
54  const children: Process[] = [];
55  env.spawn = async (...args) => {
56    const child = await spawn(...args);
57    children.push(child);
58    return child;
59  };
60  const ext = testExtension(mcp, { env });
61  await ext.configure({ configFile: "/servers.json" });
62  const server = {
63    command: Deno.execPath(),
64    args: ["run", "-A", "../server.ts"],
65    env: { TOKEN: "${MCP_TOKEN}" },
66  };
67  const configure = (servers: Record<string, unknown>) =>
68    Deno.writeTextFile(dir.path + "/servers.json", JSON.stringify({ mcpServers: servers }));
69  await configure({ probe: server });
70  return {
71    ext,
72    env,
73    children,
74    configure,
75    server,
76    root: dir.path,
77    async [Symbol.asyncDispose]() {
78      await ext.dispose();
79      await dir[Symbol.asyncDispose]();
80    },
81  };
82}
83
84Deno.test("MCP discovery is lazy, shares a connection, and calls preserve schemas, results, and progress", async () => {
85  await using f = await fixture();
86  await f.ext.prompt();
87  assertEquals(f.children.length, 0);
88  const [listed, described] = await Promise.all([
89    f.ext.call("mcp_list", {}),
90    f.ext.call("mcp_describe", { server: "probe", tool: "echo" }),
91  ]);
92  assertEquals(f.children.length, 1);
93  assertEquals((listed as { tools: { name: string }[] }[])[0].tools[0].name, "echo");
94  const description = described as { instructions: string; tools: Record<string, JsonValue>[] };
95  assertStringIncludes(description.instructions, "echo");
96  assertEquals(description.tools[0].annotations, { readOnlyHint: true });
97  assert(description.tools[0].inputSchema);
98  assert(description.tools[0].outputSchema);
99  const progress: JsonValue[] = [];
100  const result = await f.ext.call("mcp_call", {
101    server: "probe",
102    tool: "echo",
103    arguments: { fail: true, query: "猫" },
104  }, { progress: (value) => progress.push(value) }) as Result;
105  assertEquals(result.content, [{ type: "text", text: "café, 猫" }]);
106  assertEquals(result.isError, true);
107  assertEquals(result.structuredContent.arguments, { fail: true, query: "猫" });
108  assertEquals(result.structuredContent.cwd, f.root + "/project");
109  assertEquals(result.structuredContent.token, "remote-secret");
110  assertStringIncludes(result.structuredContent.roots[0].uri, "/project");
111  assertEquals((progress[0] as { progress: number }).progress, 1);
112  assertEquals(f.ext.state, { configFile: "/servers.json" });
113});
114
115Deno.test("unavailable MCP servers say why, without leaking credentials or hiding the others", async () => {
116  await using f = await fixture();
117  await f.configure({
118    probe: f.server,
119    broken: { command: "berry-mcp-no-such-program" },
120    disabled: { enabled: false, command: "${MISSING_TOKEN}" },
121    malformed: { command: "deno", args: "not-an-array" },
122    exits: {
123      command: Deno.execPath(),
124      args: ["eval", "console.error('missing API_KEY'); Deno.exit(23)"],
125    },
126    unparsable: { url: "http://[${MCP_TOKEN}]/mcp" },
127    userinfo: { url: "http://user:${MCP_TOKEN}@localhost/mcp" },
128  });
129  const listed = await within(f.ext.call("mcp_list", {})) as {
130    server: string;
131    status: string;
132    error?: string;
133  }[];
134  assertEquals(listed.map(({ server, status }) => [server, status]), [
135    ["probe", "connected"],
136    ["broken", "failed"],
137    ["disabled", "disabled"],
138    ["malformed", "failed"],
139    ["exits", "failed"],
140    ["unparsable", "failed"],
141    ["userinfo", "failed"],
142  ]);
143  assertEquals(f.children.length, 2);
144  const error = (name: string) => listed.find(({ server }) => server === name)!.error!;
145  assertStringIncludes(error("exits"), "missing API_KEY");
146  assertStringIncludes(error("exits"), "23");
147  assertStringIncludes(error("unparsable"), "URL");
148  assert(!JSON.stringify(listed).includes("remote-secret"));
149  await assertRejects(
150    () => f.ext.call("mcp_call", { server: "disabled", tool: "echo" }),
151    Error,
152    "disabled",
153  );
154  await assertRejects(
155    () => f.ext.call("mcp_describe", { server: "probe", tool: "missing" }),
156    Error,
157    "No tool",
158  );
159  await assertRejects(
160    () => f.ext.call("mcp_call", { server: "missing", tool: "echo" }),
161    Error,
162    "No MCP server",
163  );
164});
165
166Deno.test("clients follow the machine, directory, and current server configuration", async () => {
167  await using f = await fixture();
168  const call = (env?: Environment) =>
169    f.ext.call("mcp_call", { server: "probe", tool: "echo" }, { env }) as Promise<Result>;
170  const first = (await call()).structuredContent;
171  assertEquals((await call()).structuredContent.pid, first.pid);
172  f.env.cwd = "/other";
173  const moved = (await call()).structuredContent;
174  assertNotEquals(moved.pid, first.pid);
175  assertEquals(moved.cwd, f.root + "/other");
176  const another = { ...f.env, id: "another-machine", variables: { MCP_TOKEN: "another-secret" } };
177  const remote = (await call(another)).structuredContent;
178  assertNotEquals(remote.pid, moved.pid);
179  assertEquals(remote.token, "another-secret");
180  await f.configure({ probe: { ...f.server, env: { TOKEN: "changed" } } });
181  const changed = (await call()).structuredContent;
182  assertNotEquals(changed.pid, moved.pid);
183  assertEquals(changed.token, "changed");
184});
185
186Deno.test("cancellation reaches the server without discarding the shared connection", async () => {
187  await using f = await fixture();
188  const abort = new AbortController();
189  const waiting = f.ext.call("mcp_call", { server: "probe", tool: "wait" }, {
190    signal: abort.signal,
191  });
192  const rejected = assertRejects(() => waiting);
193  await until(() => Deno.stat(f.root + "/project/waiting").then(() => true, () => false));
194  abort.abort();
195  await rejected;
196  const next = await f.ext.call("mcp_call", { server: "probe", tool: "echo" }) as Result;
197  assertEquals(next.structuredContent.cancelled, 1);
198  assertEquals(f.children.length, 1);
199});
200
201Deno.test("a crashed server reconnects for the next call, never by replaying the failed call", async () => {
202  await using f = await fixture();
203  await assertRejects(() => f.ext.call("mcp_call", { server: "probe", tool: "crash" }));
204  assertEquals(f.children.length, 1);
205  const next = await Promise.all([
206    f.ext.call("mcp_call", { server: "probe", tool: "echo" }),
207    f.ext.call("mcp_call", { server: "probe", tool: "echo" }),
208  ]) as Result[];
209  assertEquals(next.map((result) => result.structuredContent.count).sort(), [1, 2]);
210  assertEquals(f.children.length, 2);
211});
212
213Deno.test("reload disposal force-stops a server that ignores stdin closure and SIGTERM", async () => {
214  await using f = await fixture();
215  await f.configure({ probe: { ...f.server, args: [...f.server.args, "stubborn"] } });
216  await f.ext.call("mcp_list", {});
217  await f.ext.dispose();
218  assertEquals(await f.children[0].exited, null);
219});
220
221Deno.test({
222  name: "reload disposal stops descendants even when their wrapper has already exited",
223  ignore: Deno.build.os === "windows",
224  async fn() {
225    await using f = await fixture();
226    await f.configure({ probe: { ...f.server, args: [...f.server.args, "descendant"] } });
227    await f.ext.call("mcp_list", {});
228    const pid = Number(await Deno.readTextFile(f.root + "/project/descendant"));
229    await f.ext.dispose();
230    await until(() => {
231      try {
232        Deno.kill(pid, 0);
233        return false;
234      } catch {
235        return true;
236      }
237    });
238  },
239});
240
241Deno.test("disposal during startup does not leave a late-spawned process running", async () => {
242  await using f = await fixture();
243  const spawn = f.env.spawn;
244  const entered = Promise.withResolvers<void>();
245  const release = Promise.withResolvers<void>();
246  f.env.spawn = async (...args) => {
247    const child = await spawn(...args);
248    entered.resolve();
249    await release.promise;
250    return child;
251  };
252  const listing = f.ext.call("mcp_list", {});
253  await entered.promise;
254  try {
255    await within(f.ext.dispose());
256    await within(listing);
257  } finally {
258    release.resolve();
259    assertEquals(await f.children[0].exited, null);
260  }
261});
262
263Deno.test("Streamable HTTP uses the environment's network and preserves MCP errors", async () => {
264  await using f = await fixture();
265  const authorization: (string | null)[] = [];
266  const methods: string[] = [];
267  const server = Deno.serve({ hostname: "127.0.0.1", port: 0, onListen() {} }, async (request) => {
268    authorization.push(request.headers.get("authorization"));
269    methods.push(request.method);
270    if (request.method === "GET") return new Response(null, { status: 405 });
271    if (request.method === "DELETE") return new Response(null, { status: 204 });
272    const message = await request.json();
273    if (message.id === undefined) return new Response(null, { status: 202 });
274    const result = message.method === "initialize"
275      ? {
276        protocolVersion: message.params.protocolVersion,
277        capabilities: { tools: {} },
278        serverInfo: { name: "http", version: "1" },
279      }
280      : message.method === "tools/list"
281      ? { tools: [{ name: "echo", inputSchema: { type: "object" } }] }
282      : { content: [], structuredContent: message.params.arguments.answer, isError: true };
283    return Response.json({ jsonrpc: "2.0", id: message.id, result }, {
284      headers: { "Mcp-Session-Id": "test-session" },
285    });
286  });
287  try {
288    let fetched = 0;
289    const fetchHere = f.env.fetch;
290    f.env.fetch = (input, init) => {
291      assertEquals(String(input), "http://only-on-the-environment.invalid/mcp");
292      fetched++;
293      return fetchHere(`http://127.0.0.1:${server.addr.port}/mcp`, init);
294    };
295    await f.configure({
296      remote: {
297        url: "http://only-on-the-environment.invalid/mcp",
298        headers: { Authorization: "Bearer ${MCP_TOKEN}" },
299      },
300    });
301    const result = await f.ext.call("mcp_call", {
302      server: "remote",
303      tool: "echo",
304      arguments: { answer: [1, "two", null] },
305    }) as { structuredContent: JsonValue; isError: boolean };
306    assertEquals(result.structuredContent, [1, "two", null]);
307    assertEquals(result.isError, true);
308    assert(fetched > 0);
309    assert(authorization.every((header) => header === "Bearer remote-secret"));
310    await f.ext.dispose();
311    assert(methods.includes("DELETE"));
312  } finally {
313    await server.shutdown();
314  }
315});
316
317Deno.test("an HTTP network failure retires the session without aborting other calls already using it", async () => {
318  await using f = await fixture();
319  const entered = Promise.withResolvers<void>();
320  const release = Promise.withResolvers<void>();
321  let initialized = 0;
322  const server = Deno.serve({ hostname: "127.0.0.1", port: 0, onListen() {} }, async (request) => {
323    if (request.method === "GET") return new Response(null, { status: 405 });
324    if (request.method === "DELETE") return new Response(null, { status: 204 });
325    const message = await request.json();
326    if (message.id === undefined) return new Response(null, { status: 202 });
327    if (message.params?.name === "slow") {
328      entered.resolve();
329      await release.promise;
330    }
331    const result = message.method === "initialize"
332      ? {
333        protocolVersion: message.params.protocolVersion,
334        capabilities: { tools: {} },
335        serverInfo: { name: "http", version: String(++initialized) },
336      }
337      : { content: [{ type: "text", text: message.params.name }] };
338    return Response.json({ jsonrpc: "2.0", id: message.id, result }, {
339      headers: { "Mcp-Session-Id": "session" },
340    });
341  });
342  const fetchHere = f.env.fetch;
343  f.env.fetch = (input, init) => {
344    const message = typeof init?.body === "string" ? JSON.parse(init.body) : undefined;
345    if (message?.params?.name === "failed") return Promise.reject(new TypeError("network lost"));
346    return fetchHere(input, init);
347  };
348  const call = (tool: string) => f.ext.call("mcp_call", { server: "http", tool });
349  try {
350    await f.configure({ http: { url: `http://127.0.0.1:${server.addr.port}/mcp` } });
351    const slow = call("slow").then((value) => ({ value }), (error: unknown) => ({ error }));
352    await within(entered.promise);
353    await assertRejects(() => call("failed"), TypeError, "network lost");
354    await call("fresh");
355    assertEquals(initialized, 2);
356    release.resolve();
357    const result = await within(slow);
358    assert("value" in result);
359    assertEquals((result.value as { content: JsonValue }).content, [{
360      type: "text",
361      text: "slow",
362    }]);
363  } finally {
364    release.resolve();
365    await f.ext.dispose();
366    await server.shutdown();
367  }
368});