@cogenta/mcp 0.1.4 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +123 -0
  2. package/dist/client/sandbox.d.ts +17 -0
  3. package/dist/client/sandbox.d.ts.map +1 -0
  4. package/dist/client/sandbox.js +24 -0
  5. package/dist/client/sandbox.js.map +1 -0
  6. package/dist/client/stdio-client.d.ts +59 -2
  7. package/dist/client/stdio-client.d.ts.map +1 -1
  8. package/dist/client/stdio-client.js +201 -25
  9. package/dist/client/stdio-client.js.map +1 -1
  10. package/dist/client/types.d.ts +21 -4
  11. package/dist/client/types.d.ts.map +1 -1
  12. package/dist/client/watchdog.d.ts +41 -0
  13. package/dist/client/watchdog.d.ts.map +1 -0
  14. package/dist/client/watchdog.js +67 -0
  15. package/dist/client/watchdog.js.map +1 -0
  16. package/dist/client/wrap-tool.d.ts +20 -0
  17. package/dist/client/wrap-tool.d.ts.map +1 -1
  18. package/dist/client/wrap-tool.js +9 -2
  19. package/dist/client/wrap-tool.js.map +1 -1
  20. package/dist/index.d.ts +13 -2
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +6 -0
  23. package/dist/index.js.map +1 -1
  24. package/dist/registry/discovery.d.ts +37 -0
  25. package/dist/registry/discovery.d.ts.map +1 -0
  26. package/dist/registry/discovery.js +68 -0
  27. package/dist/registry/discovery.js.map +1 -0
  28. package/dist/registry/store.d.ts +137 -0
  29. package/dist/registry/store.d.ts.map +1 -0
  30. package/dist/registry/store.js +272 -0
  31. package/dist/registry/store.js.map +1 -0
  32. package/dist/registry/tables.d.ts +24 -0
  33. package/dist/registry/tables.d.ts.map +1 -0
  34. package/dist/registry/tables.js +65 -0
  35. package/dist/registry/tables.js.map +1 -0
  36. package/dist/registry/tool-definitions.d.ts +53 -0
  37. package/dist/registry/tool-definitions.d.ts.map +1 -0
  38. package/dist/registry/tool-definitions.js +98 -0
  39. package/dist/registry/tool-definitions.js.map +1 -0
  40. package/package.json +3 -3
package/README.md ADDED
@@ -0,0 +1,123 @@
1
+ # `@cogenta/mcp` — connecting an MCP client to a Cogenta site
2
+
3
+ This package is the MCP (Model Context Protocol) server and client primitives
4
+ (`createMcpServer`, `serveMcpOverStdio`, `createMcpStdioClient`). The thing you
5
+ actually run against a site is the CLI command built on top of it:
6
+
7
+ ```sh
8
+ cogenta mcp
9
+ ```
10
+
11
+ It starts a real MCP server on the process's own `stdin`/`stdout` — no HTTP
12
+ port, no network listener — exposing this site's content tools (and, for an
13
+ authenticated actor, its media and site-config tools) to whatever process
14
+ spawned it: Claude Desktop, Claude Code, Cursor, or any other MCP client that
15
+ speaks stdio.
16
+
17
+ ## Who the server runs as (R4)
18
+
19
+ `cogenta mcp` never runs with an implicit admin identity. Pick one:
20
+
21
+ ```sh
22
+ cogenta mcp --email you@example.com # a real user, looked up in the site's own user store
23
+ cogenta mcp --role viewer # a synthetic actor for local testing, no real account
24
+ cogenta mcp --api-key cogenta_sk_… # a key minted from the admin's "MCP" or "Clés API" screen
25
+ cogenta mcp # anonymous ("public") — the default, no flag needed
26
+ ```
27
+
28
+ - **`--email`** resolves the account's real roles from the user store the
29
+ admin and REST API already share. The account must already exist —
30
+ `cogenta users create --email you@example.com --roles editor` first if it
31
+ doesn't. Every tool call this server makes then runs with exactly that
32
+ user's permissions, checked by the same `PermissionLayer` REST and GraphQL
33
+ use — a role that cannot publish over the admin UI cannot publish through
34
+ this server either.
35
+ - **`--role`** hands a synthetic actor with the given role(s)
36
+ (comma-separated: `--role editor,reviewer`) and no real account behind it.
37
+ Meant for local testing against a scratch site, not for pointing a real
38
+ client at a real site.
39
+ - **`--api-key`** resolves an actor from a machine-to-machine bearer key,
40
+ through the exact same `ApiKeyStore` (`@cogenta/auth`) and "roles = scope"
41
+ mapping REST's own `resolveActor` uses for a `cogenta_sk_…` bearer token —
42
+ one store, two callers. Mint one from the admin's dedicated **MCP** screen
43
+ (which also shows the client configuration below, pre-filled with the raw
44
+ key), or from the general-purpose **Clés API** screen — either works, since
45
+ a key is a key regardless of which screen minted it. A revoked, expired, or
46
+ unknown key refuses to start the server at all (`MCP_ACTOR_API_KEY_INVALID`)
47
+ rather than silently falling back to anonymous.
48
+ - **With none of these flags**, the server runs as the anonymous `public`
49
+ actor —
50
+ the same actor an unauthenticated REST request gets. Content tools
51
+ (`content.read`, `content.write_draft`, `content.publish`,
52
+ `content.delete`) are still on the manifest and still permission-checked
53
+ per call, so a public actor can read what a public actor may read and
54
+ nothing else. Media, site-config and HTTP-fetch tools are left off the
55
+ manifest entirely in this mode — see "What's on the manifest" below for
56
+ why.
57
+
58
+ ## What's on the manifest
59
+
60
+ Content tools are always on the manifest, authenticated or not, because their
61
+ actual permission gate lives one layer down: they call into the site's real
62
+ `ContentService`, which asserts against `PermissionLayer` on every read and
63
+ write, exactly as `/api/content/*` does. There is nothing extra this server
64
+ needs to enforce for them.
65
+
66
+ Media (`media.read`, `media.write`) and site-config (`site.config_read`)
67
+ tools have no such check of their own — by design, per their own doc
68
+ comments in `@cogenta/agents`, *the manifest itself* is the permission gate
69
+ for these three. `cogenta mcp` therefore only puts them on the manifest for
70
+ an authenticated actor (`--email`, `--role`, or `--api-key`); the anonymous
71
+ default never sees them at all, so there is no way to reach one without
72
+ first naming who you are running as.
73
+
74
+ ## Connecting a client
75
+
76
+ Every MCP client that supports a stdio server wants roughly the same three
77
+ things: a command, its arguments, and the working directory to run it in
78
+ (your site's root, next to `cogenta.config.mjs`).
79
+
80
+ **Claude Desktop** (`claude_desktop_config.json`) / **Claude Code**
81
+ (`.mcp.json` in your project, or `claude mcp add`) / **Cursor** (MCP settings)
82
+ all accept this shape:
83
+
84
+ ```json
85
+ {
86
+ "mcpServers": {
87
+ "cogenta": {
88
+ "command": "npx",
89
+ "args": ["cogenta", "mcp", "--email", "you@example.com"],
90
+ "cwd": "/absolute/path/to/my-site"
91
+ }
92
+ }
93
+ }
94
+ ```
95
+
96
+ Drop `--email …` for the anonymous default, swap it for `--role viewer`
97
+ while testing locally, or swap it for `--api-key cogenta_sk_…` to connect
98
+ with a key minted from the admin — the admin's **MCP** screen generates this
99
+ exact JSON block for you, with the key already filled in, right after you
100
+ create one. If `cogenta` is installed as a project dependency rather than
101
+ globally, point `command` at the local binary instead of `npx` (for example
102
+ `"command": "./node_modules/.bin/cogenta"`).
103
+
104
+ `COGENTA_AUTH_SIGNING_KEY` and any other environment variables your site's
105
+ `cogenta.config.mjs` needs must be visible to the spawned process — most
106
+ clients accept an `"env"` block alongside `"command"`/`"args"` for exactly
107
+ this; check your client's own MCP configuration docs for the exact key.
108
+
109
+ ## Known limitation
110
+
111
+ Content written or published through `cogenta mcp` goes through the same
112
+ `ContentStore` as `cogenta serve`'s REST and GraphQL routes, but **not**
113
+ through the same decorated stores `assembleSite` builds — the full-text
114
+ search index, the vector (semantic search) index, redirect tracking on a
115
+ slug rename, and scheduled-publish enqueueing are all decorators applied at
116
+ `cogenta serve` startup, and `cogenta mcp` does not currently wire them in.
117
+ An entry an MCP client creates or edits is real, permission-checked content,
118
+ immediately visible to `cogenta serve` on its next read — but it will not
119
+ appear in full-text or semantic search results, and a slug rename will not
120
+ leave a redirect, until the next full reindex (`cogenta`'s "Outils" admin
121
+ screen, or a restart that touches the entry again). See `BLOCKERS.md`,
122
+ "MCP actor scoping", for the fuller account and the reasoning behind not
123
+ gold-plating this in the first pass.
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Fiche 58 task 1bis: "`cwd`: <répertoire dédié à la connexion, vidé avant/
3
+ * après>". A third-party `stdio` MCP server is spawned with this as its
4
+ * working directory instead of `process.cwd()` (which, for `cogenta serve`,
5
+ * is the site's own project root — secrets in `.env`, the schema, the
6
+ * database file for the `sqlite` driver) — a relative-path read/write the
7
+ * remote binary attempts lands in an empty scratch directory, not the site.
8
+ *
9
+ * Created fresh on connect and removed on close/dispose: nothing the remote
10
+ * process wrote there survives past the connection it belongs to.
11
+ */
12
+ export interface SandboxWorkDir {
13
+ readonly path: string;
14
+ cleanup(): Promise<void>;
15
+ }
16
+ export declare function createSandboxWorkDir(prefix?: string): Promise<SandboxWorkDir>;
17
+ //# sourceMappingURL=sandbox.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sandbox.d.ts","sourceRoot":"","sources":["../../src/client/sandbox.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;GAUG;AAEH,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CACzB;AAED,wBAAsB,oBAAoB,CACxC,MAAM,SAAyB,GAC9B,OAAO,CAAC,cAAc,CAAC,CAmBzB"}
@@ -0,0 +1,24 @@
1
+ import { mkdtemp, rm } from 'node:fs/promises';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ export async function createSandboxWorkDir(prefix = 'cogenta-mcp-sandbox-') {
5
+ const path = await mkdtemp(join(tmpdir(), prefix));
6
+ return {
7
+ path,
8
+ async cleanup() {
9
+ // `McpClient.close()` kills the child process but does not wait for
10
+ // it to actually exit — on Windows specifically, the OS keeps a
11
+ // spawned process's working directory locked for a short window
12
+ // after `kill()` is issued, until the process has actually finished
13
+ // tearing down. A bare `rm` right after `close()` can race that and
14
+ // fail with `EBUSY`/`EPERM` — found for real running this package's
15
+ // own end-to-end test against a genuinely spawned process (never
16
+ // reproduced against the fake `spawnFn` this package's unit tests
17
+ // use, which never holds a real OS handle on `path` at all).
18
+ // `maxRetries`/`retryDelay` are `fs.rm`'s own documented answer to
19
+ // exactly this Windows race, not a bespoke retry loop.
20
+ await rm(path, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
21
+ },
22
+ };
23
+ }
24
+ //# sourceMappingURL=sandbox.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sandbox.js","sourceRoot":"","sources":["../../src/client/sandbox.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,EAAE,EAAE,MAAM,kBAAkB,CAAA;AAC9C,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAA;AAChC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAmBhC,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,MAAM,GAAG,sBAAsB;IAE/B,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,MAAM,CAAC,CAAC,CAAA;IAClD,OAAO;QACL,IAAI;QACJ,KAAK,CAAC,OAAO;YACX,oEAAoE;YACpE,gEAAgE;YAChE,gEAAgE;YAChE,oEAAoE;YACpE,oEAAoE;YACpE,oEAAoE;YACpE,iEAAiE;YACjE,kEAAkE;YAClE,6DAA6D;YAC7D,mEAAmE;YACnE,uDAAuD;YACvD,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC,EAAE,UAAU,EAAE,GAAG,EAAE,CAAC,CAAA;QAClF,CAAC;KACF,CAAA;AACH,CAAC"}
@@ -1,9 +1,58 @@
1
+ import { type Logger } from '@cogenta/core';
1
2
  import type { ChildProcessLike, McpClient } from './types.js';
3
+ import { type ReadPidUsage } from './watchdog.js';
4
+ /**
5
+ * The exact options `child_process.spawn` receives for one connection —
6
+ * pulled out so `spawnFn` (real spawn in production, a fake in tests) sees
7
+ * precisely what this client decided to hand the OS, nothing implicit.
8
+ */
9
+ export interface SpawnOptionsForClient {
10
+ readonly env: Readonly<Record<string, string>>;
11
+ readonly cwd: string;
12
+ }
2
13
  export interface McpStdioClientOptions {
3
14
  readonly command: string;
4
15
  readonly args?: readonly string[];
5
- /** Injectable for tests — defaults to `node:child_process.spawn`, piping stdio. */
6
- readonly spawnFn?: (command: string, args: readonly string[]) => ChildProcessLike;
16
+ /**
17
+ * The exact environment the spawned process receives. **Never**
18
+ * `process.env`, and never merged with it — the previous default (no
19
+ * `env` at all, which `child_process.spawn` resolves to "inherit the
20
+ * host's environment") handed a third-party binary every secret this
21
+ * process had, including `COGENTA_AUTH_SIGNING_KEY`, before the server
22
+ * had even been asked to `initialize()`. Omit this option (or pass `{}`)
23
+ * for a process that receives no environment variables at all; list
24
+ * exactly what it needs otherwise.
25
+ */
26
+ readonly env?: Readonly<Record<string, string>>;
27
+ /**
28
+ * A dedicated working directory for this one connection — see
29
+ * `./sandbox.js`'s `createSandboxWorkDir`. Required, deliberately: there
30
+ * is no safe implicit default (`process.cwd()` for `cogenta serve` is the
31
+ * site's own project root).
32
+ */
33
+ readonly cwd: string;
34
+ /**
35
+ * Where captured stderr is logged, structured, through this project's own
36
+ * redaction-aware logger — never `stdio: [...,...,'inherit']`, which
37
+ * would both bypass that logger's secret-redaction policy and let a
38
+ * hostile server inject arbitrary bytes into the host's own log stream.
39
+ * Defaults to a silent logger (still captured, just not emitted) so a
40
+ * caller that has no logger handy yet does not need to build one.
41
+ */
42
+ readonly logger?: Logger;
43
+ /** Hard ceiling on one JSON-RPC round trip (`initialize`, `tools/list`, or `tools/call`) — exceeded, the child process is killed and every pending call (this one included) rejects. A server that never responds cannot block a run indefinitely. */
44
+ readonly callTimeoutMs?: number;
45
+ /** Caps how many bytes of stderr are buffered/logged for this connection — a hostile or merely buggy server flooding stderr cannot exhaust host memory or log storage. */
46
+ readonly maxStderrBytes?: number;
47
+ /** `undefined` disables the memory ceiling. Defaults to 512 MiB — generous for a tool server, small enough to catch a runaway process. */
48
+ readonly maxRssBytes?: number;
49
+ /** `undefined` disables the CPU ceiling. Defaults to 200 (i.e. 200%, two full cores) — see `./watchdog.js`'s own module comment on why this is a floor, not a guarantee. */
50
+ readonly maxCpuPercent?: number;
51
+ readonly watchdogPollMs?: number;
52
+ /** Injectable for tests — defaults to the real `ps`/PowerShell probe (`./watchdog.js`). */
53
+ readonly readPidUsage?: ReadPidUsage;
54
+ /** Injectable for tests — defaults to `node:child_process.spawn`, always piping all three streams. */
55
+ readonly spawnFn?: (command: string, args: readonly string[], spawnOptions: SpawnOptionsForClient) => ChildProcessLike;
7
56
  }
8
57
  /**
9
58
  * The same JSON-RPC-per-line protocol `serveMcpOverStdio` (task 17) speaks,
@@ -12,6 +61,14 @@ export interface McpStdioClientOptions {
12
61
  * des serveurs tiers" — this is that consumption; `wrapMcpTool` is what
13
62
  * turns what it discovers into something the runtime can actually grant to
14
63
  * an agent, with permissions declared exactly like an internal tool.
64
+ *
65
+ * Fiche 58 task 1bis's plancher de sandboxing lives here: no implicit
66
+ * environment inheritance, no `inherit` stdio, a hard per-call timeout that
67
+ * kills the process, and a best-effort memory/CPU watchdog. None of this
68
+ * makes running an arbitrary third-party binary safe — it runs with the
69
+ * full OS privileges of the Cogenta process, always (see the registry
70
+ * layer's mandatory confirmation, `../registry/store.js`) — it only removes
71
+ * the specific, structural leaks a naive `spawn()` would otherwise have.
15
72
  */
16
73
  export declare function createMcpStdioClient(options: McpStdioClientOptions): McpClient;
17
74
  //# sourceMappingURL=stdio-client.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"stdio-client.d.ts","sourceRoot":"","sources":["../../src/client/stdio-client.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,gBAAgB,EAChB,SAAS,EAIV,MAAM,YAAY,CAAA;AAEnB,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IACjC,mFAAmF;IACnF,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,MAAM,EAAE,KAAK,gBAAgB,CAAA;CAClF;AAOD;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,qBAAqB,GAAG,SAAS,CA+D9E"}
1
+ {"version":3,"file":"stdio-client.d.ts","sourceRoot":"","sources":["../../src/client/stdio-client.ts"],"names":[],"mappings":"AAEA,OAAO,EAAgB,KAAK,MAAM,EAAE,MAAM,eAAe,CAAA;AAEzD,OAAO,KAAK,EACV,gBAAgB,EAEhB,SAAS,EAIV,MAAM,YAAY,CAAA;AACnB,OAAO,EAAoB,KAAK,YAAY,EAAoB,MAAM,eAAe,CAAA;AAErF;;;;GAIG;AACH,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,GAAG,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IAC9C,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;CACrB;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IACjC;;;;;;;;;OASG;IACH,QAAQ,CAAC,GAAG,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IAC/C;;;;;OAKG;IACH,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB;;;;;;;OAOG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IACxB,sPAAsP;IACtP,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAA;IAC/B,0KAA0K;IAC1K,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAA;IAChC,0IAA0I;IAC1I,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;IAC7B,4KAA4K;IAC5K,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAA;IAC/B,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAA;IAChC,2FAA2F;IAC3F,QAAQ,CAAC,YAAY,CAAC,EAAE,YAAY,CAAA;IACpC,sGAAsG;IACtG,QAAQ,CAAC,OAAO,CAAC,EAAE,CACjB,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,YAAY,EAAE,qBAAqB,KAChC,gBAAgB,CAAA;CACtB;AAsCD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,qBAAqB,GAAG,SAAS,CA0O9E"}
@@ -1,9 +1,35 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { createInterface } from 'node:readline';
3
3
  import { CogentaError } from '@cogenta/core';
4
- function defaultSpawn(command, args) {
5
- const child = spawn(command, [...args], { stdio: ['pipe', 'pipe', 'inherit'] });
6
- return { stdin: child.stdin, stdout: child.stdout, kill: () => child.kill() };
4
+ import { startPidWatchdog } from './watchdog.js';
5
+ const DEFAULT_CALL_TIMEOUT_MS = 30_000;
6
+ const DEFAULT_MAX_STDERR_BYTES = 64 * 1024;
7
+ const DEFAULT_MAX_RSS_BYTES = 512 * 1024 * 1024;
8
+ const DEFAULT_MAX_CPU_PERCENT = 200;
9
+ function defaultSpawn(command, args, options) {
10
+ // stdio is always fully piped — never `inherit` on stdout or stderr (fiche 58
11
+ // task 1bis): `inherit` on stderr would both bypass the structured
12
+ // logger's secret-redaction policy and let a hostile server write
13
+ // arbitrary bytes straight into the host process's own log stream.
14
+ const child = spawn(command, [...args], {
15
+ stdio: ['pipe', 'pipe', 'pipe'],
16
+ cwd: options.cwd,
17
+ env: options.env,
18
+ });
19
+ return child;
20
+ }
21
+ function silentLogger() {
22
+ const noop = () => undefined;
23
+ const logger = {
24
+ level: 'silent',
25
+ debug: noop,
26
+ info: noop,
27
+ warn: noop,
28
+ error: noop,
29
+ child: () => logger,
30
+ isLevelEnabled: () => false,
31
+ };
32
+ return logger;
7
33
  }
8
34
  /**
9
35
  * The same JSON-RPC-per-line protocol `serveMcpOverStdio` (task 17) speaks,
@@ -12,13 +38,105 @@ function defaultSpawn(command, args) {
12
38
  * des serveurs tiers" — this is that consumption; `wrapMcpTool` is what
13
39
  * turns what it discovers into something the runtime can actually grant to
14
40
  * an agent, with permissions declared exactly like an internal tool.
41
+ *
42
+ * Fiche 58 task 1bis's plancher de sandboxing lives here: no implicit
43
+ * environment inheritance, no `inherit` stdio, a hard per-call timeout that
44
+ * kills the process, and a best-effort memory/CPU watchdog. None of this
45
+ * makes running an arbitrary third-party binary safe — it runs with the
46
+ * full OS privileges of the Cogenta process, always (see the registry
47
+ * layer's mandatory confirmation, `../registry/store.js`) — it only removes
48
+ * the specific, structural leaks a naive `spawn()` would otherwise have.
15
49
  */
16
50
  export function createMcpStdioClient(options) {
17
51
  const spawnFn = options.spawnFn ?? defaultSpawn;
18
- const child = spawnFn(options.command, options.args ?? []);
52
+ const env = options.env ?? {};
53
+ const callTimeoutMs = options.callTimeoutMs ?? DEFAULT_CALL_TIMEOUT_MS;
54
+ const maxStderrBytes = options.maxStderrBytes ?? DEFAULT_MAX_STDERR_BYTES;
55
+ const maxRssBytes = options.maxRssBytes ?? DEFAULT_MAX_RSS_BYTES;
56
+ const maxCpuPercent = options.maxCpuPercent ?? DEFAULT_MAX_CPU_PERCENT;
57
+ const logger = options.logger ?? silentLogger();
58
+ const child = spawnFn(options.command, options.args ?? [], { env, cwd: options.cwd });
19
59
  const rl = createInterface({ input: child.stdout });
20
60
  const pending = new Map();
21
61
  let nextId = 1;
62
+ let closed = false;
63
+ let stderrBytesLogged = 0;
64
+ let stderrTruncated = false;
65
+ let watchdog;
66
+ function rejectAllPending(error) {
67
+ for (const [, entry] of pending)
68
+ entry.reject(error);
69
+ pending.clear();
70
+ }
71
+ function killAndRejectAll(error) {
72
+ try {
73
+ child.kill();
74
+ }
75
+ catch {
76
+ // already gone
77
+ }
78
+ rejectAllPending(error);
79
+ }
80
+ // stderr is always captured, never left to `inherit` — logged through the
81
+ // structured logger (which owns secret redaction), capped so a flood
82
+ // cannot exhaust memory or log storage.
83
+ if (child.stderr !== undefined) {
84
+ const stderrRl = createInterface({ input: child.stderr });
85
+ stderrRl.on('line', (line) => {
86
+ if (stderrBytesLogged >= maxStderrBytes) {
87
+ if (!stderrTruncated) {
88
+ stderrTruncated = true;
89
+ logger.warn('mcp client: remote stderr truncated (cap reached)', {
90
+ command: options.command,
91
+ maxStderrBytes,
92
+ });
93
+ }
94
+ return;
95
+ }
96
+ stderrBytesLogged += Buffer.byteLength(line, 'utf8');
97
+ logger.warn('mcp client: remote stderr', { command: options.command, line });
98
+ });
99
+ }
100
+ child.once?.('exit', () => {
101
+ if (closed)
102
+ return;
103
+ killAndRejectAll(new CogentaError({
104
+ code: 'MCP_CLIENT_PROCESS_EXITED',
105
+ message: 'The MCP server process exited before responding.',
106
+ hint: "Check this connection's captured stderr for why it exited.",
107
+ }));
108
+ });
109
+ child.once?.('error', (error) => {
110
+ killAndRejectAll(new CogentaError({
111
+ code: 'MCP_CLIENT_SPAWN_FAILED',
112
+ message: `Could not start the MCP server process: ${error instanceof Error ? error.message : String(error)}`,
113
+ hint: 'Check the command and arguments configured for this connection.',
114
+ cause: error,
115
+ }));
116
+ });
117
+ if (child.pid !== undefined && (maxRssBytes !== undefined || maxCpuPercent !== undefined)) {
118
+ watchdog = startPidWatchdog({
119
+ pid: child.pid,
120
+ ...(options.watchdogPollMs === undefined ? {} : { pollMs: options.watchdogPollMs }),
121
+ ...(maxRssBytes === undefined ? {} : { maxRssBytes }),
122
+ ...(maxCpuPercent === undefined ? {} : { maxCpuPercent }),
123
+ ...(options.readPidUsage === undefined ? {} : { readUsage: options.readPidUsage }),
124
+ onExceeded: (usage, reason) => {
125
+ logger.error('mcp client: resource ceiling exceeded, killing the process', {
126
+ command: options.command,
127
+ reason,
128
+ rssBytes: usage.rssBytes,
129
+ cpuPercent: usage.cpuPercent,
130
+ });
131
+ killAndRejectAll(new CogentaError({
132
+ code: 'MCP_CLIENT_RESOURCE_EXCEEDED',
133
+ message: `The MCP server process exceeded its ${reason} ceiling and was killed.`,
134
+ hint: 'This is a best-effort floor (see docs/05-securite.md) — a repeatedly misbehaving server should be disabled from the admin\'s "MCP Clients" screen.',
135
+ details: { reason, rssBytes: usage.rssBytes, cpuPercent: usage.cpuPercent },
136
+ }));
137
+ },
138
+ });
139
+ }
22
140
  rl.on('line', (line) => {
23
141
  const trimmed = line.trim();
24
142
  if (trimmed === '')
@@ -32,45 +150,103 @@ export function createMcpStdioClient(options) {
32
150
  }
33
151
  if (typeof response.id !== 'number')
34
152
  return;
35
- const resolve = pending.get(response.id);
36
- if (resolve === undefined)
153
+ const entry = pending.get(response.id);
154
+ if (entry === undefined)
37
155
  return;
38
156
  pending.delete(response.id);
39
- resolve(response);
157
+ entry.resolve(response);
40
158
  });
41
- function send(method, params) {
159
+ function send(method, params, callOptions) {
42
160
  const id = nextId++;
43
161
  return new Promise((resolve, reject) => {
44
- pending.set(id, (response) => {
45
- if ('error' in response) {
46
- reject(new CogentaError({
47
- code: 'MCP_CLIENT_REMOTE_ERROR',
48
- message: response.error.message,
49
- hint: 'The remote MCP server reported a protocol-level error, not a tool failure.',
50
- details: { code: response.error.code, data: response.error.data },
51
- }));
162
+ let settled = false;
163
+ // Deliberately does not touch `settled`/`pending` itself: `resolve`
164
+ // already clears this timer on a real response, so if this fires the
165
+ // call is genuinely still outstanding. `killAndRejectAll` rejects
166
+ // through this call's own `pending` entry below (whose `reject`
167
+ // closure is what actually settles this promise), and every other
168
+ // still-outstanding call on the same connection with it — a server
169
+ // that stops answering is presumed dead for all of them, not just
170
+ // the one that happened to hit the timeout first.
171
+ const timer = setTimeout(() => {
172
+ killAndRejectAll(new CogentaError({
173
+ code: 'MCP_CLIENT_CALL_TIMEOUT',
174
+ message: `"${method}" on the MCP server did not respond within ${callTimeoutMs}ms.`,
175
+ hint: 'The server process has been killed and every pending call on this connection has been rejected. Raise callTimeoutMs only for a server known to be legitimately slow — never to mask a hang.',
176
+ }));
177
+ }, callTimeoutMs);
178
+ timer.unref?.();
179
+ const onAbort = () => {
180
+ if (settled)
52
181
  return;
53
- }
54
- resolve(response.result);
182
+ settled = true;
183
+ clearTimeout(timer);
184
+ pending.delete(id);
185
+ reject(new CogentaError({
186
+ code: 'MCP_CLIENT_CALL_ABORTED',
187
+ message: `"${method}" was cancelled before the MCP server responded.`,
188
+ hint: "The caller's own signal was aborted — this is not a server failure.",
189
+ }));
190
+ };
191
+ callOptions?.signal?.addEventListener('abort', onAbort, { once: true });
192
+ pending.set(id, {
193
+ resolve: (response) => {
194
+ if (settled)
195
+ return;
196
+ settled = true;
197
+ clearTimeout(timer);
198
+ callOptions?.signal?.removeEventListener('abort', onAbort);
199
+ if ('error' in response) {
200
+ reject(new CogentaError({
201
+ code: 'MCP_CLIENT_REMOTE_ERROR',
202
+ message: response.error.message,
203
+ hint: 'The remote MCP server reported a protocol-level error, not a tool failure.',
204
+ details: { code: response.error.code, data: response.error.data },
205
+ }));
206
+ return;
207
+ }
208
+ resolve(response.result);
209
+ },
210
+ reject: (error) => {
211
+ if (settled)
212
+ return;
213
+ settled = true;
214
+ clearTimeout(timer);
215
+ callOptions?.signal?.removeEventListener('abort', onAbort);
216
+ reject(error);
217
+ },
55
218
  });
56
219
  child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id, method, ...(params === undefined ? {} : { params }) })}\n`);
57
220
  });
58
221
  }
59
222
  return {
60
- async initialize() {
61
- const result = (await send('initialize'));
223
+ pid: child.pid,
224
+ async initialize(callOptions) {
225
+ const result = (await send('initialize', undefined, callOptions));
62
226
  return result.serverInfo;
63
227
  },
64
- async listTools() {
65
- const result = (await send('tools/list'));
228
+ async listTools(callOptions) {
229
+ const result = (await send('tools/list', undefined, callOptions));
66
230
  return result.tools;
67
231
  },
68
- async callTool(name, args) {
69
- return (await send('tools/call', { name, arguments: args }));
232
+ async callTool(name, args, callOptions) {
233
+ return (await send('tools/call', { name, arguments: args }, callOptions));
70
234
  },
71
235
  close() {
236
+ closed = true;
237
+ watchdog?.stop();
72
238
  rl.close();
73
- child.kill();
239
+ rejectAllPending(new CogentaError({
240
+ code: 'MCP_CLIENT_CLOSED',
241
+ message: 'This MCP connection was closed.',
242
+ hint: 'Reconnect if you still need this server.',
243
+ }));
244
+ try {
245
+ child.kill();
246
+ }
247
+ catch {
248
+ // already gone
249
+ }
74
250
  },
75
251
  };
76
252
  }
@@ -1 +1 @@
1
- {"version":3,"file":"stdio-client.js","sourceRoot":"","sources":["../../src/client/stdio-client.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"stdio-client.js","sourceRoot":"","sources":["../../src/client/stdio-client.ts"],"names":[],"mappings":"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"}
@@ -16,16 +16,33 @@ export interface McpServerInfo {
16
16
  readonly name: string;
17
17
  readonly version: string;
18
18
  }
19
+ /** Per-call cancellation — distinct from the connection-wide hard timeout every call already has (fiche 58 task 1bis): a caller can cancel one in-flight call early (e.g. the agent run's own `ctx.signal`), which — like the timeout — kills the underlying process rather than leaving it running unobserved. */
20
+ export interface McpCallOptions {
21
+ readonly signal?: AbortSignal;
22
+ }
19
23
  export interface McpClient {
20
- initialize(): Promise<McpServerInfo>;
21
- listTools(): Promise<readonly McpToolSpec[]>;
22
- callTool(name: string, args: Readonly<Record<string, unknown>>): Promise<McpToolCallResult>;
24
+ initialize(options?: McpCallOptions): Promise<McpServerInfo>;
25
+ listTools(options?: McpCallOptions): Promise<readonly McpToolSpec[]>;
26
+ callTool(name: string, args: Readonly<Record<string, unknown>>, options?: McpCallOptions): Promise<McpToolCallResult>;
27
+ /** The OS process id backing this connection, when known — `undefined` for a fake used in a test. Exposed so a caller can run its own external supervision (a cgroup, a Job Object) against the real process, on top of this client's own best-effort watchdog. */
28
+ readonly pid: number | undefined;
23
29
  close(): void;
24
30
  }
25
- /** The minimal shape a spawned MCP server process needs — real `child_process.spawn()` already satisfies this; tests inject a fake. */
31
+ /**
32
+ * The minimal shape a spawned MCP server process needs — real
33
+ * `child_process.spawn()` already satisfies this structurally; tests inject
34
+ * a fake. `stderr`/`pid`/`once` are new since fiche 58 task 1bis: stdio is
35
+ * always `['pipe', 'pipe', 'pipe']` now (never `inherit`), so stderr must be
36
+ * readable from here, `pid` backs the memory/CPU watchdog, and `once` lets
37
+ * the client notice the process dying on its own (crash, kill from outside)
38
+ * instead of leaving pending calls hanging forever.
39
+ */
26
40
  export interface ChildProcessLike {
27
41
  readonly stdin: Writable;
28
42
  readonly stdout: Readable;
43
+ readonly stderr?: Readable;
44
+ readonly pid?: number;
29
45
  kill(): void;
46
+ once?(event: 'exit' | 'error', listener: (...args: unknown[]) => void): void;
30
47
  }
31
48
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/client/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AAErD,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,QAAQ,CAAC,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;CACxD;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CACtB;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,OAAO,EAAE,SAAS,eAAe,EAAE,CAAA;IAC5C,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;CAC1B;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CACzB;AAED,MAAM,WAAW,SAAS;IACxB,UAAU,IAAI,OAAO,CAAC,aAAa,CAAC,CAAA;IACpC,SAAS,IAAI,OAAO,CAAC,SAAS,WAAW,EAAE,CAAC,CAAA;IAC5C,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAA;IAC3F,KAAK,IAAI,IAAI,CAAA;CACd;AAED,uIAAuI;AACvI,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAA;IACxB,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAA;IACzB,IAAI,IAAI,IAAI,CAAA;CACb"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/client/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AAErD,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,QAAQ,CAAC,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;CACxD;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CACtB;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,OAAO,EAAE,SAAS,eAAe,EAAE,CAAA;IAC5C,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;CAC1B;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CACzB;AAED,mTAAmT;AACnT,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAA;CAC9B;AAED,MAAM,WAAW,SAAS;IACxB,UAAU,CAAC,OAAO,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC,CAAA;IAC5D,SAAS,CAAC,OAAO,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,SAAS,WAAW,EAAE,CAAC,CAAA;IACpE,QAAQ,CACN,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EACvC,OAAO,CAAC,EAAE,cAAc,GACvB,OAAO,CAAC,iBAAiB,CAAC,CAAA;IAC7B,mQAAmQ;IACnQ,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAA;IAChC,KAAK,IAAI,IAAI,CAAA;CACd;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAA;IACxB,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAA;IACzB,QAAQ,CAAC,MAAM,CAAC,EAAE,QAAQ,CAAA;IAC1B,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAA;IACrB,IAAI,IAAI,IAAI,CAAA;IACZ,IAAI,CAAC,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,EAAE,QAAQ,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,GAAG,IAAI,CAAA;CAC7E"}