pi-mono/apps/p2/test/p2/mcp-project.test.ts
Armin Ronacher 7dc38ff0e9 feat(p2): MCP servers on the host, on environments and from projects
A manager on the p2 server owns every MCP connection and process. Session
workers hold one view each and lease the locations their conversations
work at; the manager runs the owner's mcp.json servers there (stdio per
session and location, or shared; HTTP shared by default) and offers their
tools as deferred Durable tools found through tool_search.

Stdio servers on an environment start through its pi-env daemon, one
connection per environment shared by its instances, through cmd.exe on
Windows. A project's .pi/mcp.json, read by the worker at each location,
may add stdio servers that run there and switch the owner's servers on or
off; the manager refuses everything else. A lease gets servers only on
environments its session works on.
2026-11-03 16:48:38 +01:00

93 lines
3.9 KiB
TypeScript

// A project's `.pi/mcp.json` checked against the owner's servers (plugins/mcp/project.ts): what it may add (stdio
// servers at its location), what it may change (`enabled` of the owner's servers), and each rule that refuses the rest.
import assert from "node:assert/strict";
import { test } from "node:test";
import { parseOwnerConfig } from "../../plugins/mcp/config.ts";
import { parseProjectConfig } from "../../plugins/mcp/project.ts";
const OWNER = parseOwnerConfig(
JSON.stringify({
mcpServers: {
files: { command: "files-server" },
off: { command: "off-server", enabled: false },
"my-docs": { url: "https://example.com/mcp" },
},
}),
);
const project = (servers: Record<string, unknown>) =>
parseProjectConfig(JSON.stringify({ mcpServers: servers }), OWNER);
test("a project adds stdio servers and sets enabled of the owner's; host variables stay as written", () => {
const config = project({
lint: { command: "lint-server", args: ["--fix"], env: { TOKEN: `\${SECRET}` } },
later: { command: "later-server", enabled: false },
files: { enabled: false },
off: { enabled: true },
"my-docs": {},
});
assert.deepEqual(config.errors, []);
assert.deepEqual(
config.servers.map((server) => [server.name, server.config.command, server.config.args, server.config.env]),
[["lint", "lint-server", ["--fix"], { TOKEN: `\${SECRET}` }]],
);
// An entry without `enabled` is an override that changes nothing.
assert.deepEqual(
[...config.enabled],
[
["files", false],
["off", true],
],
);
});
test("each rule refuses what it should, naming the server and why; the other entries still count", () => {
const config = project({
remote: { url: "https://example.com/mcp" },
placed: { command: "x", runOn: "host" },
everywhere: { command: "x", shared: true },
signed: { command: "x", oauth: { clientId: "c" } },
provided: { command: "x", auth: { provider: "radius" } },
files: { command: "other-files" },
off: { enabled: true, args: ["x"] },
"my-docs": { enabled: "yes" },
ghost: { enabled: true },
my_docs: { command: "x" },
"bad name!": { command: "x" },
kept: { command: "kept-server" },
});
assert.deepEqual(
config.servers.map((server) => server.name),
["kept"],
);
const errors = config.errors.join("\n");
assert.equal(config.errors.length, 11, errors);
for (const error of config.errors) assert.match(error, /^\.pi\/mcp\.json: /);
assert.match(errors, /server "remote": HTTP servers are for the owner's mcp\.json/);
assert.match(errors, /server "placed": "runOn" is for the owner's mcp\.json/);
assert.match(errors, /server "everywhere": "shared" is for the owner's mcp\.json/);
assert.match(errors, /server "signed": "oauth" is for the owner's mcp\.json/);
assert.match(errors, /server "provided": "auth" is for the owner's mcp\.json/);
assert.match(errors, /server "files": the owner's mcp\.json defines it/);
assert.match(errors, /server "off": an override can only set "enabled"/);
assert.match(errors, /server "my-docs": enabled must be a boolean/);
assert.match(errors, /server "ghost": needs "command", or a server of the owner's mcp\.json to override/);
assert.match(errors, /server "my_docs": its tools would be named like those of "my-docs"/);
assert.match(errors, /invalid server name/);
assert.deepEqual([...config.enabled], []);
});
test("a file that is no configuration is one error, and nothing else", () => {
for (const [text, why] of [
["{", /Invalid \.pi\/mcp\.json syntax/],
["[]", /\.pi\/mcp\.json must be an object/],
['{ "mcpServers": [] }', /"mcpServers" must be an object/],
] as const) {
const config = parseProjectConfig(text, OWNER);
assert.deepEqual([config.servers, [...config.enabled]], [[], []]);
assert.equal(config.errors.length, 1);
assert.match(config.errors[0]!, why);
}
// Comments are allowed, as in the owner's file.
assert.deepEqual(parseProjectConfig('{ // none\n "mcpServers": {} }', OWNER).errors, []);
});