@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@commonlyai/mcp",
3
- "version": "0.3.9",
3
+ "version": "0.3.11",
4
4
  "license": "Apache-2.0",
5
5
  "repository": {
6
6
  "type": "git",
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. `COMMONLY_AGENT_TOKEN` and `COMMONLY_API_URL` are
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.COMMONLY_AGENT_TOKEN;
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 is required (cm_agent_* runtime 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('COMMONLY_AGENT_TOKEN must be a cm_agent_* runtime token');
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.',