@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.
- package/README.md +123 -0
- package/dist/client/sandbox.d.ts +17 -0
- package/dist/client/sandbox.d.ts.map +1 -0
- package/dist/client/sandbox.js +24 -0
- package/dist/client/sandbox.js.map +1 -0
- package/dist/client/stdio-client.d.ts +59 -2
- package/dist/client/stdio-client.d.ts.map +1 -1
- package/dist/client/stdio-client.js +201 -25
- package/dist/client/stdio-client.js.map +1 -1
- package/dist/client/types.d.ts +21 -4
- package/dist/client/types.d.ts.map +1 -1
- package/dist/client/watchdog.d.ts +41 -0
- package/dist/client/watchdog.d.ts.map +1 -0
- package/dist/client/watchdog.js +67 -0
- package/dist/client/watchdog.js.map +1 -0
- package/dist/client/wrap-tool.d.ts +20 -0
- package/dist/client/wrap-tool.d.ts.map +1 -1
- package/dist/client/wrap-tool.js +9 -2
- package/dist/client/wrap-tool.js.map +1 -1
- package/dist/index.d.ts +13 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/registry/discovery.d.ts +37 -0
- package/dist/registry/discovery.d.ts.map +1 -0
- package/dist/registry/discovery.js +68 -0
- package/dist/registry/discovery.js.map +1 -0
- package/dist/registry/store.d.ts +137 -0
- package/dist/registry/store.d.ts.map +1 -0
- package/dist/registry/store.js +272 -0
- package/dist/registry/store.js.map +1 -0
- package/dist/registry/tables.d.ts +24 -0
- package/dist/registry/tables.d.ts.map +1 -0
- package/dist/registry/tables.js +65 -0
- package/dist/registry/tables.js.map +1 -0
- package/dist/registry/tool-definitions.d.ts +53 -0
- package/dist/registry/tool-definitions.d.ts.map +1 -0
- package/dist/registry/tool-definitions.js +98 -0
- package/dist/registry/tool-definitions.js.map +1 -0
- 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
|
-
/**
|
|
6
|
-
|
|
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":"
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
|
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
|
|
36
|
-
if (
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
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
|
-
|
|
61
|
-
|
|
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
|
-
|
|
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":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAA;AAC1C,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;
|
|
1
|
+
{"version":3,"file":"stdio-client.js","sourceRoot":"","sources":["../../src/client/stdio-client.ts"],"names":[],"mappings":"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"}
|
package/dist/client/types.d.ts
CHANGED
|
@@ -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
|
|
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
|
-
/**
|
|
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,
|
|
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"}
|