mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
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.
49 lines
2.8 KiB
TypeScript
49 lines
2.8 KiB
TypeScript
// How a stdio server starts on a Windows environment: the pi-env daemon starts `.exe` and `.com` programs directly
|
|
// and refuses batch files (`npx` is `npx.cmd`), so every other command runs through `cmd.exe`, quoted as cross-spawn
|
|
// quotes it for Node on Windows.
|
|
|
|
import { win32 } from "node:path";
|
|
|
|
/** The argv to start and the variables to add to the program's environment. */
|
|
export type WindowsCommand = { readonly argv: readonly string[]; readonly env: Readonly<Record<string, string>> };
|
|
|
|
/** The variable holding the quoted command line `cmd.exe` runs. */
|
|
const COMMAND_VARIABLE = "P2_MCP_COMMAND";
|
|
|
|
const EXECUTABLE = /\.(?:com|exe)$/i;
|
|
|
|
const CMD_SHIM = /node_modules[\\/].bin[\\/][^\\/]+\.cmd$/i;
|
|
|
|
/**
|
|
* `command` with `args` as the daemon starts it on Windows: directly for an `.exe` or `.com` path, else through
|
|
* `cmd.exe /d /s /c`, which finds `npx.cmd` for `npx` through `PATHEXT` as a shell would.
|
|
*
|
|
* The daemon quotes every argv element itself (libuv's rules) and has no verbatim mode, so a command line in an
|
|
* argument would reach `cmd.exe` with its quotes escaped as `\"`, which `cmd.exe` does not understand. The command
|
|
* line goes in an environment variable instead, and `cmd.exe` gets `%P2_MCP_COMMAND%`, which needs no quoting: it
|
|
* expands the variable before it parses quotes and `^` escapes, so it reads the line as cross-spawn's verbatim one.
|
|
*/
|
|
export function windowsCommand(command: string, args: readonly string[]): WindowsCommand {
|
|
if (EXECUTABLE.test(command)) return { argv: [command, ...args], env: {} };
|
|
// A cmd-shim in node_modules\.bin passes its arguments through a second cmd.exe parse: their `^` escapes twice.
|
|
const quote = CMD_SHIM.test(command) ? (arg: string) => escapeCommand(escapeArgument(arg)) : escapeArgument;
|
|
const line = [escapeCommand(win32.normalize(command)), ...args.map(quote)].join(" ");
|
|
return { argv: ["cmd.exe", "/d", "/s", "/c", `%${COMMAND_VARIABLE}%`], env: { [COMMAND_VARIABLE]: line } };
|
|
}
|
|
|
|
/** cmd.exe's meta characters (cross-spawn's list, after robvanderwoude.com/escapechars.php). */
|
|
const META = /([()\][%!^"`<>&|;, *?])/g;
|
|
|
|
/** The program's name for cmd.exe: every meta character escaped with `^` (cross-spawn's `escapeCommand`). */
|
|
function escapeCommand(command: string): string {
|
|
return command.replace(META, "^$1");
|
|
}
|
|
|
|
/**
|
|
* One argument for cmd.exe and then the program's own parsing (qntm.org/cmd, as cross-spawn): backslashes before a
|
|
* quote or the end doubled, quotes escaped, the whole quoted, then every meta character escaped with `^`.
|
|
*/
|
|
function escapeArgument(arg: string): string {
|
|
// The lookahead form keeps the regular expressions from backtracking on long runs of backslashes (cross-spawn #160).
|
|
return escapeCommand(`"${arg.replace(/(?=(\\+?)?)\1"/g, '$1$1\\"').replace(/(?=(\\+?)?)\1$/, "$1$1")}"`);
|
|
}
|