agents-can-communicate 0.1.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 (105) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +176 -0
  3. package/bin/acc-hook.mjs +53 -0
  4. package/bin/acc-mcp.mjs +46 -0
  5. package/bin/acc.mjs +24 -0
  6. package/docs/CAPABILITIES.md +153 -0
  7. package/node_modules/@agents-can-communicate/adapter-claude-code/package.json +13 -0
  8. package/node_modules/@agents-can-communicate/adapter-claude-code/plugin/.claude-plugin/plugin.json +5 -0
  9. package/node_modules/@agents-can-communicate/adapter-claude-code/plugin/hooks/hooks.json +56 -0
  10. package/node_modules/@agents-can-communicate/adapter-claude-code/plugin/skills/acc/SKILL.md +217 -0
  11. package/node_modules/@agents-can-communicate/adapter-claude-code/src/adapter.mjs +64 -0
  12. package/node_modules/@agents-can-communicate/adapter-claude-code/src/hooks.mjs +106 -0
  13. package/node_modules/@agents-can-communicate/adapter-claude-code/src/install.mjs +278 -0
  14. package/node_modules/@agents-can-communicate/adapter-codex/package.json +13 -0
  15. package/node_modules/@agents-can-communicate/adapter-codex/plugin/.codex-plugin/plugin.json +17 -0
  16. package/node_modules/@agents-can-communicate/adapter-codex/plugin/hooks.json +55 -0
  17. package/node_modules/@agents-can-communicate/adapter-codex/plugin/skills/acc/SKILL.md +217 -0
  18. package/node_modules/@agents-can-communicate/adapter-codex/src/adapter.mjs +81 -0
  19. package/node_modules/@agents-can-communicate/adapter-codex/src/hooks.mjs +133 -0
  20. package/node_modules/@agents-can-communicate/adapter-codex/src/install.mjs +235 -0
  21. package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/gemini-extension.json +6 -0
  22. package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/hooks/hooks.json +69 -0
  23. package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/skills/acc/SKILL.md +217 -0
  24. package/node_modules/@agents-can-communicate/adapter-gemini-cli/package.json +13 -0
  25. package/node_modules/@agents-can-communicate/adapter-gemini-cli/src/adapter.mjs +74 -0
  26. package/node_modules/@agents-can-communicate/adapter-gemini-cli/src/hooks.mjs +109 -0
  27. package/node_modules/@agents-can-communicate/adapter-gemini-cli/src/install.mjs +149 -0
  28. package/node_modules/@agents-can-communicate/adapter-kimi/package.json +13 -0
  29. package/node_modules/@agents-can-communicate/adapter-kimi/plugin/.kimi-plugin/plugin.json +9 -0
  30. package/node_modules/@agents-can-communicate/adapter-kimi/plugin/skills/acc/SKILL.md +217 -0
  31. package/node_modules/@agents-can-communicate/adapter-kimi/src/adapter.mjs +73 -0
  32. package/node_modules/@agents-can-communicate/adapter-kimi/src/hooks.mjs +125 -0
  33. package/node_modules/@agents-can-communicate/adapter-kimi/src/install.mjs +216 -0
  34. package/node_modules/@agents-can-communicate/adapter-sdk/package.json +12 -0
  35. package/node_modules/@agents-can-communicate/adapter-sdk/src/capabilities.mjs +101 -0
  36. package/node_modules/@agents-can-communicate/adapter-sdk/src/config-merge.mjs +215 -0
  37. package/node_modules/@agents-can-communicate/adapter-sdk/src/context-projector.mjs +217 -0
  38. package/node_modules/@agents-can-communicate/adapter-sdk/src/events.mjs +55 -0
  39. package/node_modules/@agents-can-communicate/adapter-sdk/src/hook-shim.mjs +148 -0
  40. package/node_modules/@agents-can-communicate/adapter-sdk/src/index.mjs +16 -0
  41. package/node_modules/@agents-can-communicate/adapter-sdk/src/session-binding.mjs +95 -0
  42. package/node_modules/@agents-can-communicate/adapter-sdk/src/toml-block.mjs +71 -0
  43. package/node_modules/@agents-can-communicate/cli/package.json +12 -0
  44. package/node_modules/@agents-can-communicate/cli/src/args.mjs +163 -0
  45. package/node_modules/@agents-can-communicate/cli/src/claim-spelling.mjs +79 -0
  46. package/node_modules/@agents-can-communicate/cli/src/config-command.mjs +149 -0
  47. package/node_modules/@agents-can-communicate/cli/src/doctor-command.mjs +85 -0
  48. package/node_modules/@agents-can-communicate/cli/src/git-probe.mjs +41 -0
  49. package/node_modules/@agents-can-communicate/cli/src/help.mjs +78 -0
  50. package/node_modules/@agents-can-communicate/cli/src/index.mjs +10 -0
  51. package/node_modules/@agents-can-communicate/cli/src/install-command.mjs +107 -0
  52. package/node_modules/@agents-can-communicate/cli/src/main.mjs +344 -0
  53. package/node_modules/@agents-can-communicate/cli/src/platform-paths.mjs +109 -0
  54. package/node_modules/@agents-can-communicate/cli/src/runtime-paths.mjs +58 -0
  55. package/node_modules/@agents-can-communicate/cli/src/session-owner.mjs +126 -0
  56. package/node_modules/@agents-can-communicate/cli/src/workspace-discovery.mjs +151 -0
  57. package/node_modules/@agents-can-communicate/core/package.json +12 -0
  58. package/node_modules/@agents-can-communicate/core/src/claims.mjs +180 -0
  59. package/node_modules/@agents-can-communicate/core/src/communication.mjs +334 -0
  60. package/node_modules/@agents-can-communicate/core/src/index.mjs +7 -0
  61. package/node_modules/@agents-can-communicate/core/src/intents.mjs +75 -0
  62. package/node_modules/@agents-can-communicate/core/src/materialisation.mjs +86 -0
  63. package/node_modules/@agents-can-communicate/core/src/notify.mjs +95 -0
  64. package/node_modules/@agents-can-communicate/core/src/participants.mjs +48 -0
  65. package/node_modules/@agents-can-communicate/core/src/ports.mjs +56 -0
  66. package/node_modules/@agents-can-communicate/core/src/service.mjs +44 -0
  67. package/node_modules/@agents-can-communicate/core/src/sessions.mjs +190 -0
  68. package/node_modules/@agents-can-communicate/core/src/status.mjs +132 -0
  69. package/node_modules/@agents-can-communicate/core/src/sync.mjs +273 -0
  70. package/node_modules/@agents-can-communicate/core/src/tasks.mjs +238 -0
  71. package/node_modules/@agents-can-communicate/core/src/workstreams.mjs +109 -0
  72. package/node_modules/@agents-can-communicate/hook-runner/package.json +12 -0
  73. package/node_modules/@agents-can-communicate/hook-runner/src/runner.mjs +379 -0
  74. package/node_modules/@agents-can-communicate/installer/package.json +10 -0
  75. package/node_modules/@agents-can-communicate/installer/src/apply.mjs +58 -0
  76. package/node_modules/@agents-can-communicate/installer/src/detect.mjs +79 -0
  77. package/node_modules/@agents-can-communicate/installer/src/index.mjs +6 -0
  78. package/node_modules/@agents-can-communicate/installer/src/ownership.mjs +162 -0
  79. package/node_modules/@agents-can-communicate/installer/src/plan.mjs +62 -0
  80. package/node_modules/@agents-can-communicate/mcp-server/package.json +12 -0
  81. package/node_modules/@agents-can-communicate/mcp-server/src/resources.mjs +58 -0
  82. package/node_modules/@agents-can-communicate/mcp-server/src/server.mjs +250 -0
  83. package/node_modules/@agents-can-communicate/mcp-server/src/tools.mjs +201 -0
  84. package/node_modules/@agents-can-communicate/protocol/package.json +12 -0
  85. package/node_modules/@agents-can-communicate/protocol/src/config.mjs +151 -0
  86. package/node_modules/@agents-can-communicate/protocol/src/envelopes.mjs +25 -0
  87. package/node_modules/@agents-can-communicate/protocol/src/errors.mjs +30 -0
  88. package/node_modules/@agents-can-communicate/protocol/src/fields.mjs +103 -0
  89. package/node_modules/@agents-can-communicate/protocol/src/ids.mjs +25 -0
  90. package/node_modules/@agents-can-communicate/protocol/src/index.mjs +9 -0
  91. package/node_modules/@agents-can-communicate/protocol/src/resources.mjs +74 -0
  92. package/node_modules/@agents-can-communicate/protocol/src/schema.mjs +175 -0
  93. package/node_modules/@agents-can-communicate/protocol/src/states.mjs +55 -0
  94. package/node_modules/@agents-can-communicate/storage-filesystem/package.json +12 -0
  95. package/node_modules/@agents-can-communicate/storage-filesystem/src/atomic-json.mjs +135 -0
  96. package/node_modules/@agents-can-communicate/storage-filesystem/src/identity.mjs +67 -0
  97. package/node_modules/@agents-can-communicate/storage-filesystem/src/index.mjs +4 -0
  98. package/node_modules/@agents-can-communicate/storage-filesystem/src/journal.mjs +87 -0
  99. package/node_modules/@agents-can-communicate/storage-filesystem/src/record-id.mjs +44 -0
  100. package/node_modules/@agents-can-communicate/storage-filesystem/src/recovery.mjs +114 -0
  101. package/node_modules/@agents-can-communicate/storage-filesystem/src/safe-directory.mjs +80 -0
  102. package/node_modules/@agents-can-communicate/storage-filesystem/src/safe-file.mjs +55 -0
  103. package/node_modules/@agents-can-communicate/storage-filesystem/src/store.mjs +292 -0
  104. package/node_modules/@agents-can-communicate/storage-filesystem/src/writer-mutex.mjs +87 -0
  105. package/package.json +81 -0
@@ -0,0 +1,162 @@
1
+ import { createHash } from "node:crypto";
2
+ import { mkdir, readdir, readFile, rename, rm, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+
5
+ import { AccError, EXIT } from "@agents-can-communicate/protocol";
6
+
7
+ const SCHEMA_VERSION = 1;
8
+
9
+ // Installation state belongs to the machine, never to a project. A repository
10
+ // carrying it would hand one machine's paths to every clone, where none of them
11
+ // exist and all of them look like something to clean up.
12
+ const recordPath = dataHome => path.join(dataHome, "acc", "installs.json");
13
+
14
+ /**
15
+ * Content fingerprint, or null when the file is gone.
16
+ *
17
+ * Uninstall compares this against what is on disk, so ACC removes what it wrote
18
+ * and leaves what someone has since made their own. Deleting by name alone
19
+ * throws away other people's work on the strength of a path it recognises.
20
+ */
21
+ export async function fingerprint(file) {
22
+ try {
23
+ return createHash("sha256").update(await readFile(file)).digest("hex");
24
+ } catch (error) {
25
+ if (error.code === "ENOENT" || error.code === "EISDIR") return null;
26
+ throw error;
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Fingerprint of a whole directory ACC created.
32
+ *
33
+ * Every file's path and contents, in sorted order, so an edit anywhere inside a
34
+ * plugin bundle is as visible as an edit to a single file. Sorting is what makes
35
+ * it reproducible: directory order is a filesystem detail, and a hash that
36
+ * depended on it would report a modification after a harmless copy.
37
+ */
38
+ export async function treeFingerprint(root) {
39
+ let entries;
40
+ try {
41
+ entries = await readdir(root, { recursive: true, withFileTypes: true });
42
+ } catch (error) {
43
+ if (error.code === "ENOENT") return null;
44
+ if (error.code === "ENOTDIR") return fingerprint(root);
45
+ throw error;
46
+ }
47
+ const files = entries.filter(entry => entry.isFile())
48
+ .map(entry => path.relative(root, path.join(entry.parentPath ?? entry.path, entry.name)))
49
+ .sort();
50
+ const hash = createHash("sha256");
51
+ for (const relative of files) {
52
+ hash.update(relative).update("\0");
53
+ hash.update(await fingerprint(path.join(root, relative)) ?? "");
54
+ hash.update("\0");
55
+ }
56
+ return hash.digest("hex");
57
+ }
58
+
59
+ const fingerprintFor = (artifact) => (artifact.kind === "tree"
60
+ ? treeFingerprint(artifact.path)
61
+ : fingerprint(artifact.path));
62
+
63
+ export async function loadOwnership({ dataHome }) {
64
+ const file = recordPath(dataHome);
65
+ let source;
66
+ try {
67
+ source = await readFile(file, "utf8");
68
+ } catch (error) {
69
+ if (error.code === "ENOENT") return { schemaVersion: SCHEMA_VERSION, installs: [] };
70
+ throw error;
71
+ }
72
+ let record;
73
+ try {
74
+ record = JSON.parse(source);
75
+ } catch (error) {
76
+ // Treating a corrupt record as empty would make the next uninstall a no-op
77
+ // and orphan every file ACC has ever written on this machine.
78
+ throw new AccError(EXIT.DATA, "the installation record is not valid JSON",
79
+ { file, cause: error.message });
80
+ }
81
+ if (record?.schemaVersion !== SCHEMA_VERSION) {
82
+ throw new AccError(EXIT.DATA, "unknown installation record schemaVersion",
83
+ { file, schemaVersion: record?.schemaVersion ?? null });
84
+ }
85
+ return { schemaVersion: SCHEMA_VERSION, installs: record.installs ?? [] };
86
+ }
87
+
88
+ async function saveOwnership({ dataHome, record }) {
89
+ const file = recordPath(dataHome);
90
+ await mkdir(path.dirname(file), { recursive: true });
91
+ // Published by rename so a crash mid-write leaves the previous record intact
92
+ // rather than a truncated one that would fail to load at all.
93
+ const temporary = `${file}.${process.pid}.tmp`;
94
+ await writeFile(temporary, `${JSON.stringify(record, null, 2)}\n`, "utf8");
95
+ await rename(temporary, file);
96
+ }
97
+
98
+ /**
99
+ * Record what an install wrote, replacing any previous record for that adapter.
100
+ *
101
+ * Replacing rather than appending is what makes a re-run after a crash safe: the
102
+ * second run's record describes what is actually on disk now, and an accumulated
103
+ * one would list artifacts from a layout that no longer exists.
104
+ */
105
+ export async function recordInstall({ dataHome, adapterId, version, artifacts }) {
106
+ const stamped = await Promise.all(artifacts.map(async artifact => ({
107
+ path: artifact.path,
108
+ kind: artifact.kind ?? "file",
109
+ // A merge artifact is a file ACC edited but does not own, so its bytes are
110
+ // expected to change and hashing them would only ever produce a false alarm.
111
+ sha256: artifact.kind === "merge" ? null : await fingerprintFor(artifact),
112
+ })));
113
+ const record = await loadOwnership({ dataHome });
114
+ await saveOwnership({ dataHome, record: { schemaVersion: SCHEMA_VERSION,
115
+ installs: [...record.installs.filter(install => install.adapterId !== adapterId),
116
+ { adapterId, version, artifacts: stamped }] } });
117
+ }
118
+
119
+ const installFor = (record, adapterId) =>
120
+ record.installs.find(install => install.adapterId === adapterId) ?? null;
121
+
122
+ /** Compare what was written against what is there now. Read-only. */
123
+ export async function verifyOwned({ dataHome, adapterId }) {
124
+ const install = installFor(await loadOwnership({ dataHome }), adapterId);
125
+ const result = { adapterId, present: install !== null, modified: [], missing: [],
126
+ intact: [], delegated: [] };
127
+ for (const artifact of install?.artifacts ?? []) {
128
+ if (artifact.kind === "merge") { result.delegated.push(artifact.path); continue; }
129
+ const current = await fingerprintFor(artifact);
130
+ if (current === null) result.missing.push(artifact.path);
131
+ else if (current !== artifact.sha256) result.modified.push(artifact.path);
132
+ else result.intact.push(artifact.path);
133
+ }
134
+ return result;
135
+ }
136
+
137
+ /**
138
+ * Remove the files this adapter's install wrote, and only those.
139
+ *
140
+ * A modified file is kept and reported. A merge artifact is never deleted at
141
+ * all: the user owns that file and ACC owns some entries inside it, which is the
142
+ * adapter's own uninstall to unpick because it knows the format.
143
+ */
144
+ export async function removeOwned({ dataHome, adapterId }) {
145
+ const record = await loadOwnership({ dataHome });
146
+ const install = installFor(record, adapterId);
147
+ const result = { adapterId, removed: [], kept: [], missing: [], delegated: [] };
148
+ if (install === null) return result;
149
+
150
+ for (const artifact of install.artifacts) {
151
+ if (artifact.kind === "merge") { result.delegated.push(artifact.path); continue; }
152
+ const current = await fingerprintFor(artifact);
153
+ if (current === null) { result.missing.push(artifact.path); continue; }
154
+ if (current !== artifact.sha256) { result.kept.push(artifact.path); continue; }
155
+ await rm(artifact.path, { force: true, recursive: artifact.kind === "tree" });
156
+ result.removed.push(artifact.path);
157
+ }
158
+
159
+ await saveOwnership({ dataHome, record: { schemaVersion: SCHEMA_VERSION,
160
+ installs: record.installs.filter(entry => entry.adapterId !== adapterId) } });
161
+ return result;
162
+ }
@@ -0,0 +1,62 @@
1
+ import { AccError, EXIT } from "@agents-can-communicate/protocol";
2
+
3
+ /**
4
+ * Turn a detection report into exactly what would happen.
5
+ *
6
+ * Pure and deterministic: same detection, same plan, byte for byte. That is what
7
+ * makes `--dry-run` worth reading - a plan computed differently from the thing
8
+ * it previews is a decoration, and the operator would find out only afterwards.
9
+ */
10
+ export function planInstallation({ adapters, detected, context, action = "install" }) {
11
+ if (!["install", "uninstall"].includes(action)) {
12
+ throw new AccError(EXIT.USAGE, `unknown installation action: ${action}`, { action });
13
+ }
14
+ const byId = new Map(adapters.map(adapter => [adapter.id, adapter]));
15
+ const operations = [];
16
+ const skipped = [];
17
+
18
+ // Sorted by id, so two runs on the same machine produce identical JSON and a
19
+ // diff between them means something changed rather than that a registry
20
+ // enumerated in a different order.
21
+ for (const entry of [...detected].sort((a, b) => a.adapterId.localeCompare(b.adapterId))) {
22
+ const adapter = byId.get(entry.adapterId);
23
+ if (adapter === undefined) {
24
+ skipped.push({ adapterId: entry.adapterId, reason: "no adapter for this client" });
25
+ continue;
26
+ }
27
+ if (!entry.present) {
28
+ // Named rather than dropped: "nothing happened" and "that client is not
29
+ // installed on this machine" look the same in an empty list.
30
+ skipped.push({ adapterId: entry.adapterId,
31
+ reason: `${entry.displayName ?? entry.adapterId} is not installed on this machine` });
32
+ continue;
33
+ }
34
+ if (typeof adapter.planInstall !== "function") {
35
+ skipped.push({ adapterId: entry.adapterId,
36
+ reason: "this adapter cannot describe what it would write" });
37
+ continue;
38
+ }
39
+
40
+ const artifacts = adapter.planInstall(context)
41
+ .map(artifact => ({ path: artifact.path, kind: artifact.kind ?? "file" }))
42
+ .sort((a, b) => a.path.localeCompare(b.path));
43
+
44
+ operations.push({
45
+ adapterId: adapter.id,
46
+ displayName: adapter.displayName,
47
+ action,
48
+ clientVersion: entry.version ?? null,
49
+ alreadyInstalled: entry.installed === true,
50
+ artifacts,
51
+ // Said in the operator's terms, not in paths: which files ACC creates
52
+ // outright and which belong to the user and are only edited.
53
+ summary: [
54
+ ...artifacts.filter(a => a.kind === "tree")
55
+ .map(a => `${action === "install" ? "create" : "remove"} ${a.path}`),
56
+ ...artifacts.filter(a => a.kind === "merge")
57
+ .map(a => `${action === "install" ? "add ACC entries to" : "remove ACC entries from"} ${a.path}`),
58
+ ],
59
+ });
60
+ }
61
+ return { schemaVersion: 1, action, operations, skipped };
62
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "@agents-can-communicate/mcp-server",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "exports": {
7
+ ".": "./src/server.mjs"
8
+ },
9
+ "files": [
10
+ "src/"
11
+ ]
12
+ }
@@ -0,0 +1,58 @@
1
+ import { AccError, EXIT } from "@agents-can-communicate/protocol";
2
+
3
+ // Peer-authored strings reach a human's terminal through whatever renders these
4
+ // resources, so control sequences become visible escapes here rather than
5
+ // somewhere downstream. Attribution is never dropped: a message without its
6
+ // sender and type is indistinguishable from an instruction.
7
+ function escapeText(value) {
8
+ let result = "";
9
+ for (const character of String(value)) {
10
+ const code = character.codePointAt(0);
11
+ result += (code < 32 && code !== 10 && code !== 9) || code === 127
12
+ ? `\\u${code.toString(16).padStart(4, "0")}`
13
+ : character;
14
+ }
15
+ return result;
16
+ }
17
+
18
+ const attributedMessage = message => ({
19
+ messageId: message.messageId,
20
+ from: message.fromSessionId,
21
+ type: message.type,
22
+ priority: message.priority,
23
+ requiresAck: message.requiresAck,
24
+ sentAt: message.sentAt,
25
+ trust: "untrusted peer content",
26
+ subject: escapeText(message.subject),
27
+ body: escapeText(message.body),
28
+ });
29
+
30
+ export async function readResource(uri, { service, participantId, workspaceId }) {
31
+ const snapshot = await service.store.snapshot(workspaceId);
32
+ switch (uri) {
33
+ case "acc://snapshot":
34
+ return { ...snapshot,
35
+ messages: snapshot.messages.map(attributedMessage) };
36
+ case "acc://roster":
37
+ return (await service.sync({ workspaceId })).roster;
38
+ case "acc://workstreams":
39
+ return snapshot.workstreams;
40
+ case "acc://tasks":
41
+ return snapshot.tasks;
42
+ case "acc://inbox": {
43
+ const mine = new Set(snapshot.receipts
44
+ .filter(receipt => receipt.recipientParticipantId === participantId)
45
+ .map(receipt => receipt.messageId));
46
+ // A participant sees what was addressed to it, plus what it sent, so a
47
+ // fresh reader can follow its own thread.
48
+ return snapshot.messages
49
+ .filter(message => mine.has(message.messageId)
50
+ || message.toParticipantIds.includes(participantId)
51
+ || snapshot.sessions.some(session => session.sessionId === message.fromSessionId
52
+ && session.participantId === participantId))
53
+ .map(attributedMessage);
54
+ }
55
+ default:
56
+ throw new AccError(EXIT.DATA, `unknown resource: ${uri}`, { uri });
57
+ }
58
+ }
@@ -0,0 +1,250 @@
1
+ import { AccError, EXIT } from "@agents-can-communicate/protocol";
2
+ import { clearSessionBinding, loadSessionBinding, storeSessionBinding }
3
+ from "@agents-can-communicate/adapter-sdk";
4
+
5
+ import { readResource } from "./resources.mjs";
6
+ import { MCP_CAPABILITIES, PUBLIC_TOOLS, RESOURCES } from "./tools.mjs";
7
+
8
+ export const PROTOCOL_VERSION = "2026-07-28";
9
+ export const SUPPORTED_VERSIONS = Object.freeze([PROTOCOL_VERSION]);
10
+ const SERVER_INFO = Object.freeze({ name: "agents-can-communicate", version: "0.0.0" });
11
+
12
+ const META = "io.modelcontextprotocol";
13
+ const HEARTBEAT_CADENCE_MS = 60_000;
14
+
15
+ const complete = result => ({ resultType: "complete",
16
+ _meta: { [`${META}/serverInfo`]: SERVER_INFO }, ...result });
17
+
18
+ function requireProtocolMeta(params) {
19
+ const meta = params?._meta ?? {};
20
+ const version = meta[`${META}/protocolVersion`];
21
+ const capabilities = meta[`${META}/clientCapabilities`];
22
+ // The revision requires both on every request and mandates -32602 when one is
23
+ // missing. No prior request may be used to supply them.
24
+ if (typeof version !== "string" || capabilities === undefined) {
25
+ throw Object.assign(new Error(
26
+ "each request requires _meta protocolVersion and clientCapabilities"),
27
+ { rpcCode: -32602 });
28
+ }
29
+ if (!SUPPORTED_VERSIONS.includes(version)) {
30
+ throw Object.assign(new Error(`unsupported protocol version: ${version}`),
31
+ { rpcCode: -32022, rpcData: { supported: [...SUPPORTED_VERSIONS] } });
32
+ }
33
+ return { version, capabilities };
34
+ }
35
+
36
+ /**
37
+ * Resolve the ACC session for this server from its own launch configuration.
38
+ *
39
+ * Approved 2026-08-16. The protocol is stateless and forbids treating process or
40
+ * connection identity as session continuity, so the session cannot be anchored
41
+ * to the stdio process. It is derived from the participant and workspace this
42
+ * server was configured with - available identically on every request - and
43
+ * persisted through a binding so a restarted process resolves to the same
44
+ * session instead of creating a second participant.
45
+ */
46
+ async function resolveSession(context) {
47
+ const key = `mcp:${context.participantId}:${context.workspaceId}`;
48
+ const existing = await loadSessionBinding({ runtimeDir: context.runtimeDir,
49
+ harnessSessionId: key });
50
+ if (existing !== null) {
51
+ try {
52
+ return await context.service.heartbeatSession({ sessionId: existing.accSessionId,
53
+ generation: existing.generation, workspaceId: context.workspaceId });
54
+ } catch (error) {
55
+ if (error.code !== EXIT.CONFLICT) throw error;
56
+ // The recorded generation is gone. Clearing before reopening keeps the
57
+ // failure visible instead of silently accumulating bindings.
58
+ await clearSessionBinding({ runtimeDir: context.runtimeDir, harnessSessionId: key });
59
+ }
60
+ }
61
+ const session = await context.service.openSession({
62
+ workspaceId: context.workspaceId,
63
+ participantId: context.participantId,
64
+ displayName: context.participantId,
65
+ harness: "mcp",
66
+ heartbeatCadenceMs: HEARTBEAT_CADENCE_MS,
67
+ descriptor: context.descriptor,
68
+ });
69
+ await storeSessionBinding({ runtimeDir: context.runtimeDir, harnessSessionId: key,
70
+ accSessionId: session.sessionId, generation: session.generation });
71
+ return session;
72
+ }
73
+
74
+ /**
75
+ * A poll is this client's turn.
76
+ *
77
+ * The hook runtime hands a session its pending messages when it builds a turn,
78
+ * and marks them delivered. An MCP client has no turn and no hook, and no tool
79
+ * ever handed it anything: it saw a `direct_request` line carrying a subject and
80
+ * an id, and to read what a peer had actually said it had to ask for the whole
81
+ * snapshot and search every message in the workspace for its own name.
82
+ *
83
+ * The receipt never moved either. It stayed `queued` for as long as the client
84
+ * ran, so the sender was told its message had not been delivered by an agent
85
+ * that had answered it.
86
+ *
87
+ * Returning them here is delivery, in the same sense and with the same honesty
88
+ * as the turn: what is handed over is marked `injected`, and nothing else is.
89
+ * Acknowledgement stays a separate act, because being shown something is not
90
+ * agreeing to it.
91
+ */
92
+ async function syncWithMail(service, owner, context, args) {
93
+ const sync = await service.sync({ ...owner, cursor: args.cursor ?? null,
94
+ scope: args.scope, limit: args.limit });
95
+ const messages = await service.pendingMessages({
96
+ workspaceId: context.workspaceId,
97
+ participantId: context.participantId,
98
+ exceptSessionId: owner.sessionId });
99
+ if (messages.length === 0) return sync;
100
+
101
+ for (const message of messages) {
102
+ // One failure must not swallow the rest: the client is holding the message
103
+ // either way, and a receipt that cannot be written is not a reason to hide
104
+ // what a peer said.
105
+ await service.markDelivery({ ...owner, messageId: message.messageId,
106
+ state: "injected" }).catch(() => null);
107
+ }
108
+ return { ...sync, messages };
109
+ }
110
+
111
+ async function callTool(name, args, context) {
112
+ const session = await resolveSession(context);
113
+ const owner = { sessionId: session.sessionId, generation: session.generation,
114
+ workspaceId: context.workspaceId, descriptor: context.descriptor };
115
+ const service = context.service;
116
+
117
+ switch (name) {
118
+ case "acc_sync":
119
+ return syncWithMail(service, owner, context, args);
120
+ case "acc_work":
121
+ if (args.clear === true) return service.clearIntent({ ...owner });
122
+ return service.setIntent({ ...owner, summary: args.summary, mode: args.mode,
123
+ state: args.state, workstreamId: args.workstreamId ?? null,
124
+ resourceHints: args.resourceHints ?? [] });
125
+ case "acc_claim":
126
+ if (args.action === "release") return service.releaseClaim({ ...owner,
127
+ claimId: args.claimId }) ?? { released: args.claimId };
128
+ if (args.action === "renew") return service.renewClaim({ ...owner,
129
+ claimId: args.claimId, leaseSeconds: args.leaseSeconds });
130
+ return service.acquireClaim({ ...owner, resource: args.resource,
131
+ mode: args.mode ?? "exclusive", enforcement: "advisory",
132
+ reason: args.reason ?? "unspecified", leaseSeconds: args.leaseSeconds });
133
+ case "acc_message":
134
+ return service.sendMessage({ ...owner, toParticipantIds: args.to ?? [],
135
+ subject: args.subject, body: args.body, type: args.type ?? "note",
136
+ priority: args.priority, requiresAck: args.requiresAck === true,
137
+ workstreamId: args.workstreamId ?? null });
138
+ case "acc_task":
139
+ if (args.action === "claim") return service.claimTask({ ...owner,
140
+ taskId: args.taskId, force: args.force === true });
141
+ if (args.action === "decline") return service.declineTask({ ...owner,
142
+ taskId: args.taskId, reason: args.reason });
143
+ if (args.action === "transition") return service.transitionTask({ ...owner,
144
+ taskId: args.taskId, state: args.state });
145
+ return service.createTask({ ...owner, workstreamId: args.workstreamId,
146
+ title: args.title, detail: args.detail, taskId: args.taskId,
147
+ assigneeParticipantId: args.assigneeParticipantId,
148
+ dependsOn: args.dependsOn ?? [] });
149
+ case "acc_request":
150
+ return service.requestWork({ ...owner, toParticipantId: args.toParticipantId,
151
+ title: args.title, detail: args.detail, workstreamId: args.workstreamId,
152
+ priority: args.priority, dependsOn: args.dependsOn ?? [] });
153
+ case "acc_ack":
154
+ return service.markDelivery({ ...owner, messageId: args.messageId,
155
+ state: args.state ?? "acknowledged" });
156
+ case "acc_decide":
157
+ return service.recordDecision({ ...owner, title: args.title, outcome: args.outcome,
158
+ authority: args.authority ?? "workstream", workstreamId: args.workstreamId ?? null,
159
+ decidedBy: args.decidedBy, supersedes: args.supersedes ?? null,
160
+ humanConfirmed: args.humanConfirmed === true });
161
+ case "acc_workstream":
162
+ if (args.action === "coordinate") {
163
+ return service.acquireCoordinator({ ...owner, workstreamId: args.workstreamId });
164
+ }
165
+ if (args.action === "release") {
166
+ return service.releaseCoordinator({ ...owner, workstreamId: args.workstreamId });
167
+ }
168
+ return service.createWorkstream({ ...owner, title: args.title,
169
+ objective: args.objective });
170
+ case "acc_finish":
171
+ return service.finishSession({ ...owner, goal: args.goal, status: args.status,
172
+ completed: args.completed ?? [], remaining: args.remaining ?? [],
173
+ blockers: args.blockers ?? [], toParticipantId: args.toParticipantId ?? null });
174
+ default:
175
+ throw new AccError(EXIT.USAGE, `unknown tool: ${name}`, { name });
176
+ }
177
+ }
178
+
179
+ async function handle(message, context) {
180
+ const { method, params } = message;
181
+ if (method === "server/discover") {
182
+ requireProtocolMeta(params);
183
+ return complete({ supportedVersions: [...SUPPORTED_VERSIONS], capabilities: {
184
+ tools: {}, resources: {} }, serverInfo: SERVER_INFO, accCapabilities: MCP_CAPABILITIES });
185
+ }
186
+ requireProtocolMeta(params);
187
+ switch (method) {
188
+ case "tools/list":
189
+ return complete({ tools: [...PUBLIC_TOOLS] });
190
+ case "resources/list":
191
+ return complete({ resources: [...RESOURCES] });
192
+ case "resources/read": {
193
+ const value = await readResource(params.uri, context);
194
+ return complete({ contents: [{ uri: params.uri, mimeType: "application/json",
195
+ text: JSON.stringify(value, null, 2) }] });
196
+ }
197
+ case "tools/call": {
198
+ try {
199
+ const value = await callTool(params.name, params.arguments ?? {}, context);
200
+ return complete({ content: [{ type: "text", text: JSON.stringify(value, null, 2) }],
201
+ structuredContent: JSON.stringify(value) });
202
+ } catch (error) {
203
+ // A failing operation is a tool result, not a transport failure: the
204
+ // model must see it and be able to react.
205
+ return complete({ isError: true,
206
+ content: [{ type: "text", text: `${params.name}: ${error.message}` }] });
207
+ }
208
+ }
209
+ default:
210
+ throw Object.assign(new Error(`unknown method: ${method}`), { rpcCode: -32601 });
211
+ }
212
+ }
213
+
214
+ /**
215
+ * Newline-delimited JSON-RPC over the given streams. stdout carries protocol
216
+ * messages only; anything the server wants to say goes to stderr.
217
+ */
218
+ export async function serve({ input, output, log, context }) {
219
+ const write = value => output.write(`${JSON.stringify(value)}\n`);
220
+ let buffer = "";
221
+
222
+ for await (const chunk of input) {
223
+ buffer += chunk;
224
+ let index = buffer.indexOf("\n");
225
+ while (index !== -1) {
226
+ const line = buffer.slice(0, index).trim();
227
+ buffer = buffer.slice(index + 1);
228
+ index = buffer.indexOf("\n");
229
+ if (line === "") continue;
230
+ let message;
231
+ try {
232
+ message = JSON.parse(line);
233
+ } catch {
234
+ write({ jsonrpc: "2.0", error: { code: -32700, message: "parse error" } });
235
+ continue;
236
+ }
237
+ // Notifications get no reply, by rule.
238
+ if (message.id === undefined || message.id === null) continue;
239
+ try {
240
+ write({ jsonrpc: "2.0", id: message.id, result: await handle(message, context) });
241
+ } catch (error) {
242
+ log?.(`${message.method}: ${error.message}`);
243
+ write({ jsonrpc: "2.0", id: message.id, error: {
244
+ code: error.rpcCode ?? -32603,
245
+ message: error.message,
246
+ ...(error.rpcData === undefined ? {} : { data: error.rpcData }) } });
247
+ }
248
+ }
249
+ }
250
+ }