@commonlyai/mcp 0.3.9 → 0.3.11
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/package.json +1 -1
- package/src/client.js +53 -5
- package/src/tools.js +21 -0
package/package.json
CHANGED
package/src/client.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* HTTP client for the Commonly MCP server.
|
|
3
3
|
*
|
|
4
4
|
* Per ADR-010 §Auth contract:
|
|
5
|
-
* - One token per process.
|
|
5
|
+
* - One token per process. The runtime credential and `COMMONLY_API_URL` are
|
|
6
6
|
* read once at module load. Restart the host runtime to rotate.
|
|
7
7
|
* - User-Agent must NOT be the default Node `fetch` UA — Cloudflare blocks
|
|
8
8
|
* anonymous-looking clients (1010). The Python SDK hit this; same fix.
|
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
* `HttpError`. No global state beyond the env-derived config object.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
+
import { readFileSync } from 'fs';
|
|
17
|
+
|
|
16
18
|
const USER_AGENT = 'commonly-mcp/0.1.9';
|
|
17
19
|
|
|
18
20
|
export class HttpError extends Error {
|
|
@@ -24,6 +26,52 @@ export class HttpError extends Error {
|
|
|
24
26
|
}
|
|
25
27
|
}
|
|
26
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Read the runtime credential.
|
|
31
|
+
*
|
|
32
|
+
* TWO CHANNELS, in this order (TASK-078, ruled 2026-09-19 — "take the token out
|
|
33
|
+
* of the environment"):
|
|
34
|
+
*
|
|
35
|
+
* 1. `COMMONLY_TOKEN_FD` names an inherited pipe holding the credential. This
|
|
36
|
+
* is the channel the daemon now uses: the token is never in this process's
|
|
37
|
+
* environment, so a same-user reader cannot get it from
|
|
38
|
+
* `/proc/<pid>/environ` or `ps eww <pid>` — which is exactly how the old
|
|
39
|
+
* channel leaked it.
|
|
40
|
+
* 2. `COMMONLY_AGENT_TOKEN` in the environment. Kept because the channel is a
|
|
41
|
+
* property of the PARENT, not of this package: an older daemon, another
|
|
42
|
+
* driver, or a hand-run server still passes it this way, and refusing the
|
|
43
|
+
* old channel would turn a security fix into an outage.
|
|
44
|
+
*
|
|
45
|
+
* The declared fd is authoritative when present. A read that fails does NOT
|
|
46
|
+
* fall back to the environment: silently using the weaker channel after being
|
|
47
|
+
* told to use the stronger one is the class of bug this exists to remove.
|
|
48
|
+
*
|
|
49
|
+
* The fd is read to EOF and deliberately never closed — on macOS a `closeSync`
|
|
50
|
+
* after a failed read aborts the process (`kqueue.c`), and the descriptor goes
|
|
51
|
+
* away with the process anyway.
|
|
52
|
+
*/
|
|
53
|
+
export const readToken = (env = process.env, { readImpl = readFileSync } = {}) => {
|
|
54
|
+
const declaredFd = env.COMMONLY_TOKEN_FD;
|
|
55
|
+
if (declaredFd === undefined || String(declaredFd).trim() === '') {
|
|
56
|
+
return env.COMMONLY_AGENT_TOKEN;
|
|
57
|
+
}
|
|
58
|
+
const fd = Number.parseInt(String(declaredFd).trim(), 10);
|
|
59
|
+
if (!Number.isInteger(fd) || fd < 0 || String(fd) !== String(declaredFd).trim()) {
|
|
60
|
+
throw new Error(`COMMONLY_TOKEN_FD must be a file descriptor number, got '${declaredFd}'`);
|
|
61
|
+
}
|
|
62
|
+
let raw;
|
|
63
|
+
try {
|
|
64
|
+
raw = readImpl(fd, 'utf8');
|
|
65
|
+
} catch (err) {
|
|
66
|
+
throw new Error(`COMMONLY_TOKEN_FD=${fd} was declared but the credential could not be read from it: ${err.message}`);
|
|
67
|
+
}
|
|
68
|
+
const token = String(raw ?? '').trim();
|
|
69
|
+
if (!token) {
|
|
70
|
+
throw new Error(`COMMONLY_TOKEN_FD=${fd} carried an empty credential`);
|
|
71
|
+
}
|
|
72
|
+
return token;
|
|
73
|
+
};
|
|
74
|
+
|
|
27
75
|
/**
|
|
28
76
|
* Build the per-process config from env. Returned object is a frozen handle
|
|
29
77
|
* passed to `request` so the env is read exactly once and tests can inject a
|
|
@@ -32,17 +80,17 @@ export class HttpError extends Error {
|
|
|
32
80
|
* Throws on missing required env vars — better than emitting silent 401s
|
|
33
81
|
* later.
|
|
34
82
|
*/
|
|
35
|
-
export const loadConfig = (env = process.env) => {
|
|
83
|
+
export const loadConfig = (env = process.env, opts = {}) => {
|
|
36
84
|
const baseUrl = env.COMMONLY_API_URL;
|
|
37
|
-
const token = env
|
|
85
|
+
const token = readToken(env, opts);
|
|
38
86
|
if (!baseUrl) {
|
|
39
87
|
throw new Error('COMMONLY_API_URL is required (e.g. https://api.commonly.me)');
|
|
40
88
|
}
|
|
41
89
|
if (!token) {
|
|
42
|
-
throw new Error('COMMONLY_AGENT_TOKEN
|
|
90
|
+
throw new Error('No runtime token: either COMMONLY_AGENT_TOKEN must be set, or COMMONLY_TOKEN_FD must name a pipe carrying one (cm_agent_* runtime token)');
|
|
43
91
|
}
|
|
44
92
|
if (!token.startsWith('cm_agent_')) {
|
|
45
|
-
throw new Error('
|
|
93
|
+
throw new Error('The runtime token must be a cm_agent_* token, whichever channel delivered it');
|
|
46
94
|
}
|
|
47
95
|
return Object.freeze({
|
|
48
96
|
baseUrl: baseUrl.replace(/\/$/, ''),
|
package/src/tools.js
CHANGED
|
@@ -239,6 +239,27 @@ export const buildTools = (config) => {
|
|
|
239
239
|
path: `/api/agents/runtime/pods/${encodeURIComponent(podId)}/files`,
|
|
240
240
|
})),
|
|
241
241
|
},
|
|
242
|
+
{
|
|
243
|
+
name: 'commonly_list_artifacts',
|
|
244
|
+
description: 'List every file and page shared in the pods you belong to — newest first, metadata only. `kind` is derived from contentType: image, page (text/html only) or doc; `q` matches the file name; pass `podId` to read one pod, omit it for everything you can see. Page with `after` from the previous result\'s `nextCursor`. Read one with commonly_read_file (podId + fileName).',
|
|
245
|
+
inputSchema: required({
|
|
246
|
+
podId: STRING,
|
|
247
|
+
kind: { type: 'string', enum: ['image', 'page', 'doc'] },
|
|
248
|
+
q: STRING,
|
|
249
|
+
limit: INT,
|
|
250
|
+
after: STRING,
|
|
251
|
+
}),
|
|
252
|
+
call: wrap(async ({ podId, kind, q, limit, after } = {}) => {
|
|
253
|
+
const params = new URLSearchParams();
|
|
254
|
+
if (podId) params.set('podId', podId);
|
|
255
|
+
if (kind) params.set('kind', kind);
|
|
256
|
+
if (q) params.set('q', q);
|
|
257
|
+
if (limit) params.set('limit', String(limit));
|
|
258
|
+
if (after) params.set('after', after);
|
|
259
|
+
const query = params.toString();
|
|
260
|
+
return request(config, { method: 'GET', path: `/api/artifacts${query ? `?${query}` : ''}` });
|
|
261
|
+
}),
|
|
262
|
+
},
|
|
242
263
|
{
|
|
243
264
|
name: 'commonly_read_file',
|
|
244
265
|
description: 'Read the content of a file a human uploaded into a pod. Pass the `fileName` from commonly_list_files or the context `files` list. Text files (txt/md/csv/json/etc.) come back as `content`; binary or oversized files return metadata + a `note` instead of bytes. Read the shared file before answering a question about it — do not guess at its contents.',
|