pi-mono/apps/p2/test/fixtures/mcp-server.mjs
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

64 lines
3.2 KiB
JavaScript

// A stdio MCP server for p2's MCP tests, in the style of pi-mcp's test/fixtures/stdio-server.mjs. Tools: `echo`
// ({ text }), `whoami` (its pid, its parent's, cwd, the client's roots and its `MCP_FIXTURE_*` variables), `crash`
// (answers, then exits), `grow` (adds the tool `extra` and announces the change). `MCP_FIXTURE_LOG`: a file every start
// appends its pid to; `MCP_FIXTURE_HANG`: never answers `initialize`.
import { appendFileSync } from "node:fs";
import { createInterface } from "node:readline";
if (process.env.MCP_FIXTURE_LOG) appendFileSync(process.env.MCP_FIXTURE_LOG, `${process.pid}\n`);
const send = (message) => process.stdout.write(`${JSON.stringify({ jsonrpc: "2.0", ...message })}\n`);
const tools = [
{
name: "echo",
description: "Echoes the text back",
inputSchema: { type: "object", properties: { text: { type: "string" } } },
},
{ name: "whoami", description: "Reports the server process", inputSchema: { type: "object" } },
{ name: "crash", description: "Exits the server", inputSchema: { type: "object" } },
{ name: "grow", description: "Adds a tool", inputSchema: { type: "object" } },
];
let roots = null;
const lines = createInterface({ input: process.stdin, crlfDelay: Infinity });
for await (const line of lines) {
const message = JSON.parse(line);
if (message.method === "notifications/initialized") {
send({ id: "roots", method: "roots/list" });
continue;
}
if (message.id === "roots") {
roots = message.result?.roots ?? null;
continue;
}
if (!("id" in message)) continue;
if (message.method === "initialize") {
if (process.env.MCP_FIXTURE_HANG) continue;
send({
id: message.id,
result: {
protocolVersion: "2025-06-18",
capabilities: { tools: { listChanged: true } },
serverInfo: { name: "p2-fixture", version: "1.0.0" },
},
});
} else if (message.method === "tools/list") send({ id: message.id, result: { tools } });
else if (message.method === "tools/call") {
const { name, arguments: args } = message.params;
if (name === "echo")
send({ id: message.id, result: { content: [{ type: "text", text: `echo: ${args?.text}` }] } });
else if (name === "whoami") {
const vars = Object.fromEntries(Object.entries(process.env).filter(([key]) => key.startsWith("MCP_FIXTURE_")));
const text = JSON.stringify({ pid: process.pid, ppid: process.ppid, cwd: process.cwd(), roots, vars });
send({ id: message.id, result: { content: [{ type: "text", text }] } });
} else if (name === "crash") {
send({ id: message.id, result: { content: [{ type: "text", text: "bye" }] } });
console.error("fixture crashing on purpose");
setTimeout(() => process.exit(1), 20);
} else if (name === "grow") {
if (!tools.some((tool) => tool.name === "extra"))
tools.push({ name: "extra", description: "Added later", inputSchema: { type: "object" } });
send({ id: message.id, result: { content: [{ type: "text", text: "grown" }] } });
send({ method: "notifications/tools/list_changed" });
} else send({ id: message.id, result: { content: [{ type: "text", text: `no tool ${name}` }], isError: true } });
} else if (message.method === "ping") send({ id: message.id, result: {} });
else send({ id: message.id, error: { code: -32601, message: "not found" } });
}