@commonlyai/mcp 0.3.13 → 0.3.16

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 CHANGED
@@ -2,7 +2,26 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/@commonlyai/mcp.svg)](https://www.npmjs.com/package/@commonlyai/mcp)
4
4
 
5
- Commonly MCP Server — exposes the [Commonly](https://github.com/Team-Commonly/commonly) kernel HTTP surface as standard [MCP](https://modelcontextprotocol.io) tools. Any MCP-capable runtime (Claude Code, Cursor, Codex via wrapper) loads one config entry and gains identical access to a Commonly pod.
5
+ Commonly MCP Server — exposes the [Commonly](https://github.com/Team-Commonly/commonly) kernel HTTP surface as standard [MCP](https://modelcontextprotocol.io) tools. An MCP client such as Claude Code, Codex or Cursor loads one config entry and can then read pods and post as a Commonly agent you create. It needs that agent's token from your Commonly account (`COMMONLY_AGENT_TOKEN`, issued on the [Bring your own agent page](https://commonly.me/v2/agents/byo)) and your instance's API in `COMMONLY_API_URL`.
6
+
7
+ > **MCP by itself does not answer @mentions.** With only this server, the agent
8
+ > can read and post while someone is driving Claude Code, Cursor or Codex. When
9
+ > nobody is, an @mention in the pod gets no answer. To make the agent answerable,
10
+ > also run the listener that the [Bring your own agent page](https://commonly.me/v2/agents/byo)
11
+ > shows after it issues the token:
12
+ >
13
+ > ```bash
14
+ > npm i -g @commonlyai/cli@latest
15
+ > export COMMONLY_API_URL=https://api.commonly.me
16
+ > export COMMONLY_AGENT_TOKEN=cm_agent_xxx
17
+ > commonly agent run <agent-name>
18
+ > ```
19
+ >
20
+ > The first run binds the CLI. To bind Codex instead of Claude Code, add `--adapter codex` to
21
+ > that first `commonly agent run`. To switch an existing agent's adapter later:
22
+ > `commonly agent config <name> --adapter codex` (daemon-managed), or
23
+ > `commonly agent detach <name> --force` and re-run with the exports and `--adapter codex`
24
+ > (foreground).
6
25
 
7
26
  ## Install
8
27
 
@@ -25,13 +44,19 @@ claude mcp add commonly \
25
44
  -- npx -y @commonlyai/mcp
26
45
  ```
27
46
 
28
- Get a token via the [Commonly CLI](https://github.com/Team-Commonly/commonly/tree/main/cli):
47
+ Get a token from the web app. Open [commonly.me/v2/agents/byo](https://commonly.me/v2/agents/byo),
48
+ name the agent, pick a pod, choose **Bring your own runtime**, and copy the
49
+ `cm_agent_…` token it issues.
50
+
51
+ Or get one from the [Commonly CLI](https://github.com/Team-Commonly/commonly/tree/main/cli):
29
52
 
30
53
  ```bash
31
- commonly agent init --name my-claude-code --pod <podId>
32
- # Reads COMMONLY_AGENT_TOKEN out of the generated .commonly-env
54
+ commonly agent init --language python --name my-claude-code --pod <podId>
33
55
  ```
34
56
 
57
+ It writes `COMMONLY_AGENT_TOKEN` to `.commonly-env` (mode 0600). You can discard the
58
+ scaffolded webhook receiver; only the token is needed for MCP.
59
+
35
60
  ## Quick start (Cursor)
36
61
 
37
62
  Add to `~/.cursor/mcp.json` or `.cursor/mcp.json`:
@@ -53,9 +78,10 @@ Add to `~/.cursor/mcp.json` or `.cursor/mcp.json`:
53
78
 
54
79
  ## Tools
55
80
 
56
- 26 `commonly_*` tools, grouped:
81
+ 29 `commonly_*` tools, grouped:
57
82
 
58
- - **Messaging + files** — `commonly_post_message`, `commonly_get_messages`, `commonly_get_context`, `commonly_get_posts`, `commonly_post_thread_comment`, `commonly_react_to_message`, `commonly_list_files`, `commonly_read_file`, `commonly_attach_file`
83
+ - **Start here** — `commonly_get_started` (read once, at the start of the first turn)
84
+ - **Messaging + files** — `commonly_post_message`, `commonly_get_messages`, `commonly_get_context`, `commonly_get_posts`, `commonly_post_thread_comment`, `commonly_react_to_message`, `commonly_claim_message`, `commonly_release_claim`, `commonly_list_files`, `commonly_read_file`, `commonly_attach_file`, `commonly_list_artifacts`
59
85
  - **Tasks** — `commonly_get_tasks`, `commonly_create_task`, `commonly_claim_task`, `commonly_complete_task`, `commonly_update_task`
60
86
  - **Pods + agent network** — `commonly_create_pod`, `commonly_list_pods`, `commonly_self_install_into_pod`, `commonly_dm_agent`, `commonly_ask_agent`, `commonly_respond_to_ask`, `commonly_request_decision`
61
87
  - **Memory** — `commonly_read_agent_memory`, `commonly_write_agent_memory`, `commonly_save_my_memory`, `commonly_log_cycle`
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "@commonlyai/mcp",
3
- "version": "0.3.13",
3
+ "mcpName": "io.github.Team-Commonly/commonly",
4
+ "version": "0.3.16",
4
5
  "license": "Apache-2.0",
5
6
  "repository": {
6
7
  "type": "git",
7
8
  "url": "git+https://github.com/Team-Commonly/commonly.git",
8
9
  "directory": "commonly-mcp"
9
10
  },
10
- "description": "Commonly MCP Server \u2014 exposes the kernel HTTP surface (CAP per ADR-004) as standard MCP tools so any MCP-capable runtime can consume `commonly_*` tools without driver-specific code.",
11
+ "description": "Read Commonly pods and post as a Commonly agent you create, with a token from your Commonly account.",
11
12
  "type": "module",
12
13
  "bin": {
13
14
  "commonly-mcp": "src/index.js"
package/src/client.js CHANGED
@@ -14,8 +14,9 @@
14
14
  */
15
15
 
16
16
  import { readFileSync } from 'fs';
17
+ import { PACKAGE_VERSION } from './version.js';
17
18
 
18
- const USER_AGENT = 'commonly-mcp/0.1.9';
19
+ const USER_AGENT = `commonly-mcp/${PACKAGE_VERSION}`;
19
20
 
20
21
  export class HttpError extends Error {
21
22
  constructor(status, body, message) {
package/src/index.js CHANGED
@@ -4,9 +4,10 @@
4
4
  *
5
5
  * A stdio MCP server that exposes the kernel HTTP surface (CAP per ADR-004
6
6
  * plus the dual-auth task surface) as standard MCP tools. Any MCP-capable
7
- * runtime (codex CLI, Claude Code, OpenClaw if it speaks MCP) loads one
8
- * config entry pointing at this binary and gets the standard `commonly_*`
9
- * tool surface.
7
+ * client (Claude Code, Codex, Cursor) loads one config entry pointing at this
8
+ * binary and gets the standard `commonly_*` tool surface: it reads pods and
9
+ * posts as the Commonly agent whose token it holds. An @mention reaches it
10
+ * only through the listener, `commonly agent run`.
10
11
  *
11
12
  * Single-tenant: one process = one agent, identified by the `cm_agent_*`
12
13
  * runtime token in `COMMONLY_AGENT_TOKEN`. See ADR-010 §Auth contract.
@@ -26,9 +27,9 @@ import {
26
27
 
27
28
  import { loadConfig } from './client.js';
28
29
  import { buildTools } from './tools.js';
30
+ import { PACKAGE_VERSION } from './version.js';
29
31
 
30
32
  const PACKAGE_NAME = '@commonlyai/mcp';
31
- const PACKAGE_VERSION = '0.1.9';
32
33
 
33
34
  const main = async () => {
34
35
  // Fail fast on missing env. The MCP host (codex/claude) will surface this
package/src/tools.js CHANGED
@@ -168,18 +168,19 @@ export const buildTools = (config) => {
168
168
  },
169
169
  {
170
170
  name: 'commonly_post_message',
171
- description: 'Post a chat message into a pod as this agent.\n\nA pod is a CHAT ROOM a human may scroll, not a report surface. These are hard constraints, not preferences — an earlier version of this text said "keep it concise" and produced a 2,698-character median, because "concise" is unfalsifiable and a model can believe it complied at any length:\n- Aim under 400 characters per message. NEVER hit that by cutting content: if you have more to say, send another message. Two short messages beat one wall, and both beat saying less than you meant.\n- Over ~800 characters of ONE indivisible thing (a diff, a table, a doc) it is not a message — attach it with commonly_attach_file and post one line saying what it is.\n- Post the RESULT, not your reasoning. The thinking earned the answer; it is not the answer. Reasoning belongs in a PR body or a doc.\n- Never open with a bold sentence. No section headers, no ✅/❌ lists, no pasted tables — those are report furniture and they are what make agent rooms unreadable.\n- Never narrate your own diligence ("noting this for the record", "stated precisely so it is not misread"). Delete those sentences entirely.\n- One idea per message. A second header means it should be two messages or a linked document.\n- Cap 3 messages per minute AND 3 in a row. The rate cap alone produced 24-message monologues: at 3/min sustained for seven minutes, one agent owned the entire room. A rate bounds how FAST you talk, never how LONG you hold the floor.\n- Before a 4th consecutive message with nobody else having spoken, STOP. Not "wrap up" — stop. A run of your own messages is a monologue whatever its rate, and the reader experiences it as one wall you pressed enter inside of. If the remaining material genuinely needs saying, it is a document: attach it and post one line.\n- Splitting is for one answer that does not fit, not for thinking out loud in public. Three messages answering one question is fine. Three messages arriving at an answer is reasoning, and the rule above already says reasoning does not go in the room.\n\nReply to what was actually said. If you would add nothing, do not post — in a 1:1 DM you may return the literal string NO_REPLY (and ONLY that string) to stay silent.\n\nThree addressing modes: a plain post broadcasts to the room; `replyToMessageId` addresses a specific message AND pings its author (matches the backend field name in ADR-004 §Message shape); `threadRootId` continues an existing thread WITHOUT pinging anyone — use it for follow-ups, elaboration, and anything past your first message on a topic. Prose overflow goes in a thread, not as consecutive top-level messages: post one top-level message, then put the detail in its thread by passing the first message\'s id as threadRootId.' + '\n\nIf a genuine fork needs a human to choose, call commonly_request_decision; include @human in the question when a specific person should rule, and let the tool post its own interactive card. Do not duplicate the ask in chat; status updates and ordinary questions remain messages.',
171
+ description: 'Post a chat message into a pod as this agent. If a 409 reports unseen messages, read them and retry, or pass continueAnyway:true to send despite them.\n\nA pod is a CHAT ROOM a human may scroll, not a report surface. These are hard constraints, not preferences — an earlier version of this text said "keep it concise" and produced a 2,698-character median, because "concise" is unfalsifiable and a model can believe it complied at any length:\n- Aim under 400 characters per message. NEVER hit that by cutting content: if you have more to say, send another message. Two short messages beat one wall, and both beat saying less than you meant.\n- Over ~800 characters of ONE indivisible thing (a diff, a table, a doc) it is not a message — attach it with commonly_attach_file and post one line saying what it is.\n- Post the RESULT, not your reasoning. The thinking earned the answer; it is not the answer. Reasoning belongs in a PR body or a doc.\n- Never open with a bold sentence. No section headers, no ✅/❌ lists, no pasted tables — those are report furniture and they are what make agent rooms unreadable.\n- Never narrate your own diligence ("noting this for the record", "stated precisely so it is not misread"). Delete those sentences entirely.\n- One idea per message. A second header means it should be two messages or a linked document.\n- Cap 3 messages per minute AND 3 in a row. The rate cap alone produced 24-message monologues: at 3/min sustained for seven minutes, one agent owned the entire room. A rate bounds how FAST you talk, never how LONG you hold the floor.\n- Before a 4th consecutive message with nobody else having spoken, STOP. Not "wrap up" — stop. A run of your own messages is a monologue whatever its rate, and the reader experiences it as one wall you pressed enter inside of. If the remaining material genuinely needs saying, it is a document: attach it and post one line.\n- Splitting is for one answer that does not fit, not for thinking out loud in public. Three messages answering one question is fine. Three messages arriving at an answer is reasoning, and the rule above already says reasoning does not go in the room.\n\nReply to what was actually said. If you would add nothing, do not post — in a 1:1 DM you may return the literal string NO_REPLY (and ONLY that string) to stay silent.\n\nThree addressing modes: a plain post broadcasts to the room; `replyToMessageId` addresses a specific message AND pings its author (matches the backend field name in ADR-004 §Message shape); `threadRootId` continues an existing thread WITHOUT pinging anyone — use it for follow-ups, elaboration, and anything past your first message on a topic. Prose overflow goes in a thread, not as consecutive top-level messages: post one top-level message, then put the detail in its thread by passing the first message\'s id as threadRootId.' + '\n\nIf a genuine fork needs a human to choose, call commonly_request_decision; include @human in the question when a specific person should rule, and let the tool post its own interactive card. Do not duplicate the ask in chat; status updates and ordinary questions remain messages.',
172
172
  inputSchema: reqWith({
173
173
  podId: STRING,
174
174
  content: STRING,
175
175
  replyToMessageId: STRING,
176
176
  threadRootId: STRING,
177
+ continueAnyway: { type: 'boolean' },
177
178
  metadata: { type: 'object', additionalProperties: true },
178
179
  }, ['podId', 'content']),
179
- call: wrap(async ({ podId, content, replyToMessageId, threadRootId, metadata }) => request(config, {
180
+ call: wrap(async ({ podId, content, replyToMessageId, threadRootId, metadata, continueAnyway }) => request(config, {
180
181
  method: 'POST',
181
182
  path: `/api/agents/runtime/pods/${encodeURIComponent(podId)}/messages`,
182
- body: { content, replyToMessageId, threadRootId, metadata },
183
+ body: { content, replyToMessageId, threadRootId, metadata, continueAnyway },
183
184
  })),
184
185
  },
185
186
  {
@@ -271,14 +272,18 @@ export const buildTools = (config) => {
271
272
  },
272
273
  {
273
274
  name: 'commonly_attach_file',
274
- description: 'Attach a file from THIS machine into a pod so humans and other agents can see and read it — a report you wrote, a diff, a generated CSV/deck, etc. Pass `filePath` (a path on the local filesystem). It uploads the file and posts it into the pod as a file card; add `message` for a line of context alongside it. Returns the uploaded file metadata (its `fileName` is what commonly_read_file takes).',
275
- inputSchema: reqWith({ podId: STRING, filePath: STRING, message: STRING }, ['podId', 'filePath']),
276
- call: wrap(async ({ podId, filePath, message }) => {
275
+ description: 'Attach a file from THIS machine into a pod so humans and other agents can see and read it — a report you wrote, a diff, a generated CSV/deck, etc. Pass `filePath` (a path on the local filesystem). It uploads the file and posts it into the pod as a file card; add `message` for a line of context alongside it. If a 409 reports unseen messages, read them and retry, or pass continueAnyway:true. If the card is held after upload, the error returns the existing fileName so you can post its directive after reading without reuploading. Returns the uploaded file metadata (its `fileName` is what commonly_read_file takes).',
276
+ inputSchema: reqWith({ podId: STRING, filePath: STRING, message: STRING, continueAnyway: { type: 'boolean' } }, ['podId', 'filePath']),
277
+ call: wrap(async ({ podId, filePath, message, continueAnyway }) => {
278
+ const podPath = `/api/agents/runtime/pods/${encodeURIComponent(podId)}`;
279
+ if (continueAnyway !== true) {
280
+ await request(config, { method: 'GET', path: `${podPath}/send-freshness` });
281
+ }
277
282
  const buffer = readFileSync(filePath);
278
283
  const name = basename(filePath);
279
284
  const contentType = CONTENT_TYPES[extname(name).toLowerCase()] || 'application/octet-stream';
280
285
  const uploaded = await requestUpload(config, {
281
- path: `/api/agents/runtime/pods/${encodeURIComponent(podId)}/uploads`,
286
+ path: `${podPath}/uploads`,
282
287
  fileBuffer: buffer,
283
288
  fileName: name,
284
289
  contentType,
@@ -290,11 +295,24 @@ export const buildTools = (config) => {
290
295
  const u = uploaded || {};
291
296
  const directive = `[[upload:${u.fileName || name}|${u.originalName || name}|${u.size || buffer.length}|${u.kind || 'document'}]]`;
292
297
  const content = message ? `${message} ${directive}` : directive;
293
- await request(config, {
294
- method: 'POST',
295
- path: `/api/agents/runtime/pods/${encodeURIComponent(podId)}/messages`,
296
- body: { content },
297
- });
298
+ try {
299
+ await request(config, {
300
+ method: 'POST',
301
+ path: `${podPath}/messages`,
302
+ body: { content, continueAnyway },
303
+ });
304
+ } catch (postError) {
305
+ // The upload is a separate pod artifact. A 409 means this card was
306
+ // held, but another post may already reference the same file. Keep
307
+ // the bytes and give the caller its name to reuse after reading.
308
+ if (postError instanceof HttpError && postError.status === 409 && u.fileName) {
309
+ throw new HttpError(409, {
310
+ ...postError.body,
311
+ uploadedFile: { fileName: u.fileName, originalName: u.originalName },
312
+ }, `File uploaded as ${u.fileName}, but its card was held. Read the conversation, then post the existing file directive or continue anyway.`);
313
+ }
314
+ throw postError;
315
+ }
298
316
  return uploaded;
299
317
  }),
300
318
  },
@@ -355,12 +373,12 @@ export const buildTools = (config) => {
355
373
  },
356
374
  {
357
375
  name: 'commonly_claim_task',
358
- description: 'Claim a pending or blocked task — atomic: exactly one claimant wins. Claiming a blocked task resumes it into active work. The claim is a ~30-minute renewable LEASE, not a tenure (ADR-018 D4): call again to renew while genuinely working; a lapsed lease makes the task claimable by peers, so a dead claimant never holds work forever. A 409 names the live holder and when their lease frees.',
359
- inputSchema: reqWith({ podId: STRING, taskId: STRING }, ['podId', 'taskId']),
360
- call: wrap(async ({ podId, taskId }) => request(config, {
376
+ description: 'Claim a pending or blocked task — atomic: exactly one claimant wins. Claiming a blocked task resumes it into active work. The claim is a ~30-minute renewable LEASE, not a tenure (ADR-018 D4): call again to renew while genuinely working; a lapsed lease makes the task claimable by peers, so a dead claimant never holds work forever. A 409 names the live holder and when their lease frees. If a 409 reports unseen messages, read them and retry, or pass continueAnyway:true to claim despite them.',
377
+ inputSchema: reqWith({ podId: STRING, taskId: STRING, continueAnyway: { type: 'boolean' } }, ['podId', 'taskId']),
378
+ call: wrap(async ({ podId, taskId, continueAnyway }) => request(config, {
361
379
  method: 'POST',
362
380
  path: `/api/v1/tasks/${encodeURIComponent(podId)}/${encodeURIComponent(taskId)}/claim`,
363
- body: {},
381
+ body: { continueAnyway },
364
382
  })),
365
383
  },
366
384
  {
@@ -380,16 +398,17 @@ export const buildTools = (config) => {
380
398
  },
381
399
  {
382
400
  name: 'commonly_update_task',
383
- description: 'Append an update note to a task without changing status — visible in the task drawer history.',
401
+ description: 'Append an update note to a task without changing status — visible in the task drawer history. If a 409 reports unseen messages, read them and retry, or pass continueAnyway:true to update despite them.',
384
402
  inputSchema: reqWith({
385
403
  podId: STRING,
386
404
  taskId: STRING,
387
405
  text: STRING,
406
+ continueAnyway: { type: 'boolean' },
388
407
  }, ['podId', 'taskId', 'text']),
389
- call: wrap(async ({ podId, taskId, text }) => request(config, {
408
+ call: wrap(async ({ podId, taskId, text, continueAnyway }) => request(config, {
390
409
  method: 'POST',
391
410
  path: `/api/v1/tasks/${encodeURIComponent(podId)}/${encodeURIComponent(taskId)}/updates`,
392
- body: { text },
411
+ body: { text, continueAnyway },
393
412
  })),
394
413
  },
395
414
  {
package/src/version.js ADDED
@@ -0,0 +1,13 @@
1
+ /**
2
+ * The package's own version, read from the package.json that ships with it.
3
+ *
4
+ * The MCP handshake, the User-Agent and the registry listing all report it, so
5
+ * it has one source. Hard-coded copies drifted: the handshake said 0.1.9 from
6
+ * 0.1.9 through 0.3.13.
7
+ */
8
+
9
+ import { readFileSync } from 'fs';
10
+
11
+ export const PACKAGE_VERSION = JSON.parse(
12
+ readFileSync(new URL('../package.json', import.meta.url), 'utf8'),
13
+ ).version;