agent-coord-mcp 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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 David Balzan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,118 @@
1
+ # agent-coord-mcp
2
+
3
+ A tiny file-backed [MCP](https://modelcontextprotocol.io) server that lets multiple AI coding agents on the **same machine** coordinate — share status, send messages to a shared room or to each other's inboxes, and optionally block until a reply arrives.
4
+
5
+ State lives in `~/agent-coord/` as JSONL/JSON files, so you can `tail -f` the conversation in any terminal.
6
+
7
+ > **Local-only** — coordination happens through the local filesystem. Agents need to share the same `~/agent-coord/` directory (i.e. same machine, same user). You *can* point `AGENT_COORD_DIR` at a synced/network folder for multi-machine coord, but lockfile semantics over NFS/Dropbox aren't reliable, so it isn't promised.
8
+ >
9
+ > **Works with any MCP client.** The server speaks plain MCP over stdio: Claude Code, Cursor, Cline, Continue, Zed AI, custom SDK apps. Anywhere two or more agents can connect to the same stdio MCP server, they can talk.
10
+ >
11
+ > No auth, no encryption. Anything that can read your home directory can read the messages.
12
+
13
+ ## Install
14
+
15
+ ```sh
16
+ git clone https://github.com/davidbalzan/agent-coord-mcp.git
17
+ cd agent-coord-mcp
18
+ npm install # runs `npm run build` automatically via `prepare`
19
+ ```
20
+
21
+ The built entrypoint is `dist/server.js`.
22
+
23
+ ## Connect a client
24
+
25
+ Each client just needs to launch `node /path/to/agent-coord-mcp/dist/server.js` over stdio.
26
+
27
+ ### Claude Code
28
+
29
+ ```sh
30
+ claude mcp add --scope user agent-coord -- node /absolute/path/to/agent-coord-mcp/dist/server.js
31
+ ```
32
+
33
+ Or edit `~/.claude.json` directly:
34
+
35
+ ```json
36
+ {
37
+ "mcpServers": {
38
+ "agent-coord": {
39
+ "command": "node",
40
+ "args": ["/absolute/path/to/agent-coord-mcp/dist/server.js"]
41
+ }
42
+ }
43
+ }
44
+ ```
45
+
46
+ ### Cursor / Cline / Continue / Zed / etc.
47
+
48
+ These all use a similar `mcpServers` config block. Drop in the same `command` + `args` shape. Refer to your client's MCP docs for the exact file.
49
+
50
+ ### Custom client (Python / TS)
51
+
52
+ If you're building an agent with the official MCP SDKs (`@modelcontextprotocol/sdk` in TS, `mcp` in Python), spawn the server as a stdio subprocess and call the tools below — no editor required.
53
+
54
+ ## Tools
55
+
56
+ | Tool | Purpose |
57
+ | --- | --- |
58
+ | `register({agentId, project?, role?})` | Announce yourself in `agents.json`. Call once per session. |
59
+ | `heartbeat({agentId})` | Refresh your `lastHeartbeat`. |
60
+ | `list_agents()` | See all known agents and which look online (heartbeat <5min). |
61
+ | `send_message({from, to?, room?, text})` | If `to` set → that agent's inbox. Else → shared room. |
62
+ | `read_messages({agentId, source, limit?, peek?, sinceTs?})` | Read new messages. `source` is `inbox`/`room`/`status`. Advances cursor unless `peek:true`. |
63
+ | `post_status({agentId, status, detail?})` | Append to the shared status stream (separate from chat). |
64
+ | `wait_for_message({agentId, source, timeoutMs?})` | Block (max 60s) until a new entry appears, then return it. |
65
+ | `prune({olderThanDays?, removeOrphanInboxes?, dryRun?})` | Trim room/status/inbox JSONL to entries newer than N days (default 7). Removes inbox files for agents no longer in the registry. Pass `dryRun:true` to preview. |
66
+
67
+ ## Convention for agent IDs
68
+
69
+ Use the project's directory name or a short stable slug (e.g. `frontend`, `api`, `worker`). Tell each agent — in its `CLAUDE.md`, system prompt, or however your client supports persistent instructions — something like:
70
+
71
+ > Your coord agentId is `frontend`. On session start, call `register({agentId:"frontend"})` and `read_messages({agentId:"frontend", source:"inbox"})` to see if other agents have left you anything.
72
+
73
+ ## Tail it from a terminal
74
+
75
+ ```sh
76
+ # shared room
77
+ tail -f ~/agent-coord/room.jsonl
78
+
79
+ # a specific agent's inbox
80
+ tail -f ~/agent-coord/inbox/frontend.jsonl
81
+
82
+ # status broadcasts
83
+ tail -f ~/agent-coord/status.jsonl
84
+
85
+ # pretty-print live
86
+ tail -f ~/agent-coord/room.jsonl | jq -c '{ts: (.ts/1000|todate), from, to, text}'
87
+ ```
88
+
89
+ ## Files on disk
90
+
91
+ ```
92
+ ~/agent-coord/
93
+ agents.json # registry
94
+ room.jsonl # shared chat
95
+ status.jsonl # status broadcasts
96
+ inbox/<agentId>.jsonl # per-agent inboxes
97
+ cursors/<agentId>.json # last-read offsets
98
+ ```
99
+
100
+ To reset everything: `rm -rf ~/agent-coord && mkdir -p ~/agent-coord/{inbox,cursors}`.
101
+
102
+ ### Cleanup
103
+
104
+ The registry auto-evicts agents whose last heartbeat is older than 24h on every `list_agents` call. For chat history and inbox trimming, call `prune` periodically (e.g. weekly) — it's safe to run from any agent and supports `dryRun`.
105
+
106
+ ## Override location
107
+
108
+ Set `AGENT_COORD_DIR=/some/other/path` in the MCP server's env to relocate state. (`CLAUDE_COORD_DIR` is also honored as a legacy alias.) Useful if you want different agent groups isolated, or to put the dir on a synced volume so agents on different machines can collaborate (caveat above).
109
+
110
+ ## Realtime vs. polling
111
+
112
+ `wait_for_message` is the cheap path: one tool call, server-side `fs.watch` + 500ms poll, capped at 60s. The model only pays for one round-trip per wait.
113
+
114
+ The model is fundamentally turn-based — there's no async push that wakes an idle agent. For *passive* presence (react when pinged, even between user turns), wire a client-side hook that runs `read_messages --peek` and injects unread inbox entries into the next prompt. With Claude Code that's a `UserPromptSubmit` hook; other clients have similar mechanisms.
115
+
116
+ ## License
117
+
118
+ MIT — see [LICENSE](./LICENSE).
package/dist/server.js ADDED
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { ensureDirs } from "./store.js";
5
+ import { heartbeatSchema, heartbeatTool, listAgentsSchema, listAgentsTool, postStatusSchema, postStatusTool, pruneSchema, pruneTool, readMessagesSchema, readMessagesTool, registerSchema, registerTool, sendMessageSchema, sendMessageTool, waitForMessageSchema, waitForMessageTool, } from "./tools.js";
6
+ function jsonResult(data) {
7
+ return {
8
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
9
+ };
10
+ }
11
+ async function main() {
12
+ ensureDirs();
13
+ const server = new McpServer({
14
+ name: "agent-coord",
15
+ version: "0.1.0",
16
+ });
17
+ server.tool("register", "Register this agent in the shared registry. Call once per session.", registerSchema, async (args) => jsonResult(await registerTool(args)));
18
+ server.tool("heartbeat", "Refresh this agent's lastHeartbeat timestamp.", heartbeatSchema, async (args) => jsonResult(await heartbeatTool(args)));
19
+ server.tool("list_agents", "List all known agents and whether they appear online (heartbeat <5min).", listAgentsSchema, async () => jsonResult(await listAgentsTool()));
20
+ server.tool("send_message", "Send a message. If 'to' is set, goes to that agent's inbox; otherwise to the shared room.", sendMessageSchema, async (args) => jsonResult(await sendMessageTool(args)));
21
+ server.tool("read_messages", "Read new messages from inbox|room|status. Advances the cursor unless peek=true.", readMessagesSchema, async (args) => jsonResult(await readMessagesTool(args)));
22
+ server.tool("post_status", "Append a status broadcast to the shared status stream.", postStatusSchema, async (args) => jsonResult(await postStatusTool(args)));
23
+ server.tool("prune", "Trim room/status/inbox JSONL to entries newer than `olderThanDays` (default 7). Removes inbox files for agents no longer in the registry unless removeOrphanInboxes=false. Pass dryRun=true to preview.", pruneSchema, async (args) => jsonResult(await pruneTool(args)));
24
+ server.tool("wait_for_message", "Block (max 60s) until a new message appears on the given source, then return it.", waitForMessageSchema, async (args) => jsonResult(await waitForMessageTool(args)));
25
+ const transport = new StdioServerTransport();
26
+ await server.connect(transport);
27
+ }
28
+ main().catch((err) => {
29
+ console.error("[agent-coord-mcp] fatal:", err);
30
+ process.exit(1);
31
+ });
32
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
package/dist/store.js ADDED
@@ -0,0 +1,160 @@
1
+ import { promises as fs, existsSync, mkdirSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import path from "node:path";
4
+ import lockfile from "proper-lockfile";
5
+ export const ROOT = process.env.AGENT_COORD_DIR ??
6
+ process.env.CLAUDE_COORD_DIR ??
7
+ path.join(homedir(), "agent-coord");
8
+ export const AGENTS_FILE = path.join(ROOT, "agents.json");
9
+ export const ROOM_FILE = path.join(ROOT, "room.jsonl");
10
+ export const STATUS_FILE = path.join(ROOT, "status.jsonl");
11
+ export const INBOX_DIR = path.join(ROOT, "inbox");
12
+ export const CURSOR_DIR = path.join(ROOT, "cursors");
13
+ export function ensureDirs() {
14
+ for (const d of [ROOT, INBOX_DIR, CURSOR_DIR]) {
15
+ if (!existsSync(d))
16
+ mkdirSync(d, { recursive: true });
17
+ }
18
+ for (const f of [ROOM_FILE, STATUS_FILE]) {
19
+ if (!existsSync(f))
20
+ mkdirSync(path.dirname(f), { recursive: true });
21
+ }
22
+ }
23
+ async function ensureFile(file) {
24
+ if (!existsSync(file)) {
25
+ await fs.mkdir(path.dirname(file), { recursive: true });
26
+ await fs.writeFile(file, "", "utf8");
27
+ }
28
+ }
29
+ async function withLock(file, fn) {
30
+ await ensureFile(file);
31
+ const release = await lockfile.lock(file, {
32
+ retries: { retries: 10, minTimeout: 20, maxTimeout: 200 },
33
+ stale: 5000,
34
+ });
35
+ try {
36
+ return await fn();
37
+ }
38
+ finally {
39
+ await release();
40
+ }
41
+ }
42
+ export async function appendJsonl(file, entry) {
43
+ await withLock(file, async () => {
44
+ const line = JSON.stringify(entry) + "\n";
45
+ await fs.appendFile(file, line, "utf8");
46
+ });
47
+ }
48
+ export async function readJsonl(file) {
49
+ if (!existsSync(file))
50
+ return [];
51
+ const raw = await fs.readFile(file, "utf8");
52
+ const out = [];
53
+ for (const line of raw.split("\n")) {
54
+ if (!line.trim())
55
+ continue;
56
+ try {
57
+ out.push(JSON.parse(line));
58
+ }
59
+ catch {
60
+ // skip malformed line
61
+ }
62
+ }
63
+ return out;
64
+ }
65
+ export async function readJson(file, fallback) {
66
+ if (!existsSync(file))
67
+ return fallback;
68
+ try {
69
+ const raw = await fs.readFile(file, "utf8");
70
+ if (!raw.trim())
71
+ return fallback;
72
+ return JSON.parse(raw);
73
+ }
74
+ catch {
75
+ return fallback;
76
+ }
77
+ }
78
+ export async function writeJson(file, data) {
79
+ await withLock(file, async () => {
80
+ await fs.writeFile(file, JSON.stringify(data, null, 2), "utf8");
81
+ });
82
+ }
83
+ async function readJsonNoLock(file, fallback) {
84
+ if (!existsSync(file))
85
+ return fallback;
86
+ try {
87
+ const raw = await fs.readFile(file, "utf8");
88
+ if (!raw.trim())
89
+ return fallback;
90
+ return JSON.parse(raw);
91
+ }
92
+ catch {
93
+ return fallback;
94
+ }
95
+ }
96
+ export async function updateJson(file, fallback, mutate) {
97
+ return withLock(file, async () => {
98
+ const current = await readJsonNoLock(file, fallback);
99
+ const next = await mutate(current);
100
+ await fs.writeFile(file, JSON.stringify(next, null, 2), "utf8");
101
+ return next;
102
+ });
103
+ }
104
+ export function inboxFile(agentId) {
105
+ return path.join(INBOX_DIR, `${sanitize(agentId)}.jsonl`);
106
+ }
107
+ export function cursorFile(agentId) {
108
+ return path.join(CURSOR_DIR, `${sanitize(agentId)}.json`);
109
+ }
110
+ function sanitize(id) {
111
+ return id.replace(/[^a-zA-Z0-9._-]/g, "_");
112
+ }
113
+ export async function rewriteJsonl(file, filter) {
114
+ if (!existsSync(file))
115
+ return { kept: 0, removed: 0 };
116
+ return withLock(file, async () => {
117
+ const raw = await fs.readFile(file, "utf8");
118
+ let kept = 0;
119
+ let removed = 0;
120
+ const out = [];
121
+ for (const line of raw.split("\n")) {
122
+ if (!line.trim())
123
+ continue;
124
+ try {
125
+ const entry = JSON.parse(line);
126
+ if (filter(entry)) {
127
+ out.push(line);
128
+ kept++;
129
+ }
130
+ else {
131
+ removed++;
132
+ }
133
+ }
134
+ catch {
135
+ removed++;
136
+ }
137
+ }
138
+ await fs.writeFile(file, out.length ? out.join("\n") + "\n" : "", "utf8");
139
+ return { kept, removed };
140
+ });
141
+ }
142
+ export async function deleteFile(file) {
143
+ if (!existsSync(file))
144
+ return false;
145
+ await fs.unlink(file);
146
+ return true;
147
+ }
148
+ export async function listInboxFiles() {
149
+ if (!existsSync(INBOX_DIR))
150
+ return [];
151
+ const names = await fs.readdir(INBOX_DIR);
152
+ return names.filter((n) => n.endsWith(".jsonl"));
153
+ }
154
+ export async function fileSize(file) {
155
+ if (!existsSync(file))
156
+ return 0;
157
+ const st = await fs.stat(file);
158
+ return st.size;
159
+ }
160
+ //# sourceMappingURL=store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"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"}
package/dist/tools.js ADDED
@@ -0,0 +1,273 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { watch } from "node:fs";
3
+ import { z } from "zod";
4
+ import path from "node:path";
5
+ import { AGENTS_FILE, INBOX_DIR, ROOM_FILE, STATUS_FILE, appendJsonl, cursorFile, deleteFile, fileSize, inboxFile, listInboxFiles, readJson, readJsonl, rewriteJsonl, updateJson, } from "./store.js";
6
+ const STALE_MS = 5 * 60 * 1000;
7
+ const EVICT_MS = 24 * 60 * 60 * 1000;
8
+ const MAX_WAIT_MS = 60_000;
9
+ // ---------- register ----------
10
+ export const registerSchema = {
11
+ agentId: z.string().min(1),
12
+ project: z.string().optional(),
13
+ role: z.string().optional(),
14
+ };
15
+ export async function registerTool(args) {
16
+ const reg = await updateJson(AGENTS_FILE, {}, (current) => {
17
+ const now = Date.now();
18
+ const existing = current[args.agentId];
19
+ current[args.agentId] = {
20
+ agentId: args.agentId,
21
+ project: args.project ?? existing?.project,
22
+ role: args.role ?? existing?.role,
23
+ registeredAt: existing?.registeredAt ?? now,
24
+ lastHeartbeat: now,
25
+ };
26
+ return current;
27
+ });
28
+ return { ok: true, agent: reg[args.agentId] };
29
+ }
30
+ // ---------- heartbeat ----------
31
+ export const heartbeatSchema = { agentId: z.string().min(1) };
32
+ export async function heartbeatTool(args) {
33
+ let missing = false;
34
+ await updateJson(AGENTS_FILE, {}, (current) => {
35
+ if (!current[args.agentId]) {
36
+ missing = true;
37
+ return current;
38
+ }
39
+ current[args.agentId].lastHeartbeat = Date.now();
40
+ return current;
41
+ });
42
+ if (missing)
43
+ return { ok: false, error: `agent '${args.agentId}' not registered` };
44
+ return { ok: true };
45
+ }
46
+ // ---------- list_agents ----------
47
+ export const listAgentsSchema = {};
48
+ export async function listAgentsTool() {
49
+ const now = Date.now();
50
+ const evicted = [];
51
+ const reg = await updateJson(AGENTS_FILE, {}, (current) => {
52
+ for (const [id, entry] of Object.entries(current)) {
53
+ if (now - entry.lastHeartbeat > EVICT_MS) {
54
+ evicted.push(id);
55
+ delete current[id];
56
+ }
57
+ }
58
+ return current;
59
+ });
60
+ const agents = Object.values(reg).map((a) => ({
61
+ ...a,
62
+ online: now - a.lastHeartbeat < STALE_MS,
63
+ secondsSinceHeartbeat: Math.floor((now - a.lastHeartbeat) / 1000),
64
+ }));
65
+ return { agents, evicted };
66
+ }
67
+ // ---------- send_message ----------
68
+ export const sendMessageSchema = {
69
+ from: z.string().min(1),
70
+ to: z.string().optional(),
71
+ room: z.string().optional(),
72
+ text: z.string().min(1),
73
+ };
74
+ export async function sendMessageTool(args) {
75
+ const msg = {
76
+ id: randomUUID(),
77
+ ts: Date.now(),
78
+ from: args.from,
79
+ to: args.to,
80
+ room: args.room,
81
+ text: args.text,
82
+ };
83
+ const target = args.to ? inboxFile(args.to) : ROOM_FILE;
84
+ await appendJsonl(target, msg);
85
+ return { ok: true, id: msg.id, target };
86
+ }
87
+ // ---------- read_messages ----------
88
+ export const readMessagesSchema = {
89
+ agentId: z.string().min(1),
90
+ source: z.enum(["inbox", "room", "status"]),
91
+ limit: z.number().int().positive().max(500).optional(),
92
+ peek: z.boolean().optional(),
93
+ sinceTs: z.number().optional(),
94
+ };
95
+ export async function readMessagesTool(args) {
96
+ const file = sourceFile(args.source, args.agentId);
97
+ const offsetKey = offsetKeyFor(args.source);
98
+ const all = await readJsonl(file);
99
+ let limited = [];
100
+ let totalNew = 0;
101
+ if (args.peek) {
102
+ const cursor = await readJson(cursorFile(args.agentId), {});
103
+ const startOffset = cursor[offsetKey] ?? 0;
104
+ let entries = all.slice(startOffset);
105
+ if (args.sinceTs !== undefined)
106
+ entries = entries.filter((e) => e.ts > args.sinceTs);
107
+ totalNew = entries.length;
108
+ limited = args.limit ? entries.slice(0, args.limit) : entries;
109
+ }
110
+ else {
111
+ await updateJson(cursorFile(args.agentId), {}, (current) => {
112
+ const startOffset = current[offsetKey] ?? 0;
113
+ let entries = all.slice(startOffset);
114
+ if (args.sinceTs !== undefined)
115
+ entries = entries.filter((e) => e.ts > args.sinceTs);
116
+ totalNew = entries.length;
117
+ limited = args.limit ? entries.slice(0, args.limit) : entries;
118
+ if (limited.length > 0)
119
+ current[offsetKey] = startOffset + limited.length;
120
+ return current;
121
+ });
122
+ }
123
+ return { messages: limited, totalNew, returned: limited.length };
124
+ }
125
+ // ---------- post_status ----------
126
+ export const postStatusSchema = {
127
+ agentId: z.string().min(1),
128
+ status: z.string().min(1),
129
+ detail: z.string().optional(),
130
+ };
131
+ export async function postStatusTool(args) {
132
+ const entry = {
133
+ id: randomUUID(),
134
+ ts: Date.now(),
135
+ agentId: args.agentId,
136
+ status: args.status,
137
+ detail: args.detail,
138
+ };
139
+ await appendJsonl(STATUS_FILE, entry);
140
+ return { ok: true, id: entry.id };
141
+ }
142
+ // ---------- wait_for_message ----------
143
+ export const waitForMessageSchema = {
144
+ agentId: z.string().min(1),
145
+ source: z.enum(["inbox", "room", "status"]),
146
+ timeoutMs: z.number().int().positive().max(MAX_WAIT_MS).optional(),
147
+ };
148
+ export async function waitForMessageTool(args) {
149
+ const timeout = Math.min(args.timeoutMs ?? 30_000, MAX_WAIT_MS);
150
+ const file = sourceFile(args.source, args.agentId);
151
+ const startSize = await fileSize(file);
152
+ const result = await new Promise((resolve) => {
153
+ let settled = false;
154
+ const finish = (changed) => {
155
+ if (settled)
156
+ return;
157
+ settled = true;
158
+ clearInterval(poll);
159
+ try {
160
+ watcher?.close();
161
+ }
162
+ catch {
163
+ // ignore
164
+ }
165
+ clearTimeout(t);
166
+ resolve({ changed });
167
+ };
168
+ const check = async () => {
169
+ const sz = await fileSize(file);
170
+ if (sz > startSize)
171
+ finish(true);
172
+ };
173
+ let watcher;
174
+ try {
175
+ watcher = watch(file, () => {
176
+ void check();
177
+ });
178
+ }
179
+ catch {
180
+ // file may not exist; polling will handle
181
+ }
182
+ const poll = setInterval(() => void check(), 500);
183
+ const t = setTimeout(() => finish(false), timeout);
184
+ });
185
+ if (!result.changed) {
186
+ return { ok: false, timedOut: true };
187
+ }
188
+ return readMessagesTool({ agentId: args.agentId, source: args.source });
189
+ }
190
+ // ---------- prune ----------
191
+ export const pruneSchema = {
192
+ olderThanDays: z.number().positive().max(365).optional(),
193
+ removeOrphanInboxes: z.boolean().optional(),
194
+ dryRun: z.boolean().optional(),
195
+ };
196
+ export async function pruneTool(args) {
197
+ const days = args.olderThanDays ?? 7;
198
+ const cutoff = Date.now() - days * 24 * 60 * 60 * 1000;
199
+ const dryRun = args.dryRun ?? false;
200
+ const reg = await readJson(AGENTS_FILE, {});
201
+ const knownAgents = new Set(Object.keys(reg));
202
+ if (dryRun) {
203
+ const room = await readJsonl(ROOM_FILE);
204
+ const status = await readJsonl(STATUS_FILE);
205
+ const inboxFiles = await listInboxFiles();
206
+ let inboxRemoved = 0;
207
+ const orphans = [];
208
+ for (const fname of inboxFiles) {
209
+ const id = fname.replace(/\.jsonl$/, "");
210
+ if (!knownAgents.has(id) && (args.removeOrphanInboxes ?? true))
211
+ orphans.push(fname);
212
+ const entries = await readJsonl(path.join(INBOX_DIR, fname));
213
+ inboxRemoved += entries.filter((e) => e.ts <= cutoff).length;
214
+ }
215
+ return {
216
+ dryRun: true,
217
+ cutoff,
218
+ olderThanDays: days,
219
+ wouldRemove: {
220
+ roomMessages: room.filter((e) => e.ts <= cutoff).length,
221
+ statusEntries: status.filter((e) => e.ts <= cutoff).length,
222
+ inboxMessages: inboxRemoved,
223
+ orphanInboxes: orphans,
224
+ },
225
+ };
226
+ }
227
+ const roomResult = await rewriteJsonl(ROOM_FILE, (e) => e.ts > cutoff);
228
+ const statusResult = await rewriteJsonl(STATUS_FILE, (e) => e.ts > cutoff);
229
+ const inboxFiles = await listInboxFiles();
230
+ let inboxRemoved = 0;
231
+ const deletedOrphans = [];
232
+ for (const fname of inboxFiles) {
233
+ const id = fname.replace(/\.jsonl$/, "");
234
+ const filePath = path.join(INBOX_DIR, fname);
235
+ if (!knownAgents.has(id) && (args.removeOrphanInboxes ?? true)) {
236
+ const entries = await readJsonl(filePath);
237
+ inboxRemoved += entries.length;
238
+ await deleteFile(filePath);
239
+ deletedOrphans.push(id);
240
+ continue;
241
+ }
242
+ const r = await rewriteJsonl(filePath, (e) => e.ts > cutoff);
243
+ inboxRemoved += r.removed;
244
+ }
245
+ return {
246
+ dryRun: false,
247
+ cutoff,
248
+ olderThanDays: days,
249
+ removed: {
250
+ roomMessages: roomResult.removed,
251
+ statusEntries: statusResult.removed,
252
+ inboxMessages: inboxRemoved,
253
+ orphanInboxes: deletedOrphans,
254
+ },
255
+ note: "Cursors not adjusted; clients may see stale offsets but read_messages clamps via slice().",
256
+ };
257
+ }
258
+ // ---------- helpers ----------
259
+ function sourceFile(source, agentId) {
260
+ if (source === "inbox")
261
+ return inboxFile(agentId);
262
+ if (source === "room")
263
+ return ROOM_FILE;
264
+ return STATUS_FILE;
265
+ }
266
+ function offsetKeyFor(source) {
267
+ if (source === "inbox")
268
+ return "inboxOffset";
269
+ if (source === "room")
270
+ return "roomOffset";
271
+ return "statusOffset";
272
+ }
273
+ //# sourceMappingURL=tools.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tools.js","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "agent-coord-mcp",
3
+ "version": "0.1.0",
4
+ "description": "File-backed MCP server for coordinating multiple AI coding agents (Claude Code, Cursor, Cline, etc.) on the same machine.",
5
+ "type": "module",
6
+ "bin": {
7
+ "agent-coord-mcp": "dist/server.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "prepare": "tsc",
12
+ "start": "node dist/server.js",
13
+ "dev": "tsx src/server.ts"
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "src",
18
+ "README.md",
19
+ "LICENSE"
20
+ ],
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "https://github.com/davidbalzan/agent-coord-mcp.git"
24
+ },
25
+ "license": "MIT",
26
+ "dependencies": {
27
+ "@modelcontextprotocol/sdk": "^1.0.4",
28
+ "proper-lockfile": "^4.1.2",
29
+ "zod": "^3.23.8"
30
+ },
31
+ "devDependencies": {
32
+ "@types/node": "^22.10.0",
33
+ "@types/proper-lockfile": "^4.1.4",
34
+ "tsx": "^4.19.2",
35
+ "typescript": "^5.7.2"
36
+ }
37
+ }
package/src/server.ts ADDED
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { ensureDirs } from "./store.js";
5
+ import {
6
+ heartbeatSchema,
7
+ heartbeatTool,
8
+ listAgentsSchema,
9
+ listAgentsTool,
10
+ postStatusSchema,
11
+ postStatusTool,
12
+ pruneSchema,
13
+ pruneTool,
14
+ readMessagesSchema,
15
+ readMessagesTool,
16
+ registerSchema,
17
+ registerTool,
18
+ sendMessageSchema,
19
+ sendMessageTool,
20
+ waitForMessageSchema,
21
+ waitForMessageTool,
22
+ } from "./tools.js";
23
+
24
+ function jsonResult(data: unknown) {
25
+ return {
26
+ content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }],
27
+ };
28
+ }
29
+
30
+ async function main() {
31
+ ensureDirs();
32
+
33
+ const server = new McpServer({
34
+ name: "agent-coord",
35
+ version: "0.1.0",
36
+ });
37
+
38
+ server.tool(
39
+ "register",
40
+ "Register this agent in the shared registry. Call once per session.",
41
+ registerSchema,
42
+ async (args) => jsonResult(await registerTool(args))
43
+ );
44
+
45
+ server.tool(
46
+ "heartbeat",
47
+ "Refresh this agent's lastHeartbeat timestamp.",
48
+ heartbeatSchema,
49
+ async (args) => jsonResult(await heartbeatTool(args))
50
+ );
51
+
52
+ server.tool(
53
+ "list_agents",
54
+ "List all known agents and whether they appear online (heartbeat <5min).",
55
+ listAgentsSchema,
56
+ async () => jsonResult(await listAgentsTool())
57
+ );
58
+
59
+ server.tool(
60
+ "send_message",
61
+ "Send a message. If 'to' is set, goes to that agent's inbox; otherwise to the shared room.",
62
+ sendMessageSchema,
63
+ async (args) => jsonResult(await sendMessageTool(args))
64
+ );
65
+
66
+ server.tool(
67
+ "read_messages",
68
+ "Read new messages from inbox|room|status. Advances the cursor unless peek=true.",
69
+ readMessagesSchema,
70
+ async (args) => jsonResult(await readMessagesTool(args))
71
+ );
72
+
73
+ server.tool(
74
+ "post_status",
75
+ "Append a status broadcast to the shared status stream.",
76
+ postStatusSchema,
77
+ async (args) => jsonResult(await postStatusTool(args))
78
+ );
79
+
80
+ server.tool(
81
+ "prune",
82
+ "Trim room/status/inbox JSONL to entries newer than `olderThanDays` (default 7). Removes inbox files for agents no longer in the registry unless removeOrphanInboxes=false. Pass dryRun=true to preview.",
83
+ pruneSchema,
84
+ async (args) => jsonResult(await pruneTool(args))
85
+ );
86
+
87
+ server.tool(
88
+ "wait_for_message",
89
+ "Block (max 60s) until a new message appears on the given source, then return it.",
90
+ waitForMessageSchema,
91
+ async (args) => jsonResult(await waitForMessageTool(args))
92
+ );
93
+
94
+ const transport = new StdioServerTransport();
95
+ await server.connect(transport);
96
+ }
97
+
98
+ main().catch((err) => {
99
+ console.error("[agent-coord-mcp] fatal:", err);
100
+ process.exit(1);
101
+ });
package/src/store.ts ADDED
@@ -0,0 +1,161 @@
1
+ import { promises as fs, existsSync, mkdirSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import path from "node:path";
4
+ import lockfile from "proper-lockfile";
5
+
6
+ export const ROOT =
7
+ process.env.AGENT_COORD_DIR ??
8
+ process.env.CLAUDE_COORD_DIR ??
9
+ path.join(homedir(), "agent-coord");
10
+ export const AGENTS_FILE = path.join(ROOT, "agents.json");
11
+ export const ROOM_FILE = path.join(ROOT, "room.jsonl");
12
+ export const STATUS_FILE = path.join(ROOT, "status.jsonl");
13
+ export const INBOX_DIR = path.join(ROOT, "inbox");
14
+ export const CURSOR_DIR = path.join(ROOT, "cursors");
15
+
16
+ export function ensureDirs(): void {
17
+ for (const d of [ROOT, INBOX_DIR, CURSOR_DIR]) {
18
+ if (!existsSync(d)) mkdirSync(d, { recursive: true });
19
+ }
20
+ for (const f of [ROOM_FILE, STATUS_FILE]) {
21
+ if (!existsSync(f)) mkdirSync(path.dirname(f), { recursive: true });
22
+ }
23
+ }
24
+
25
+ async function ensureFile(file: string): Promise<void> {
26
+ if (!existsSync(file)) {
27
+ await fs.mkdir(path.dirname(file), { recursive: true });
28
+ await fs.writeFile(file, "", "utf8");
29
+ }
30
+ }
31
+
32
+ async function withLock<T>(file: string, fn: () => Promise<T>): Promise<T> {
33
+ await ensureFile(file);
34
+ const release = await lockfile.lock(file, {
35
+ retries: { retries: 10, minTimeout: 20, maxTimeout: 200 },
36
+ stale: 5000,
37
+ });
38
+ try {
39
+ return await fn();
40
+ } finally {
41
+ await release();
42
+ }
43
+ }
44
+
45
+ export async function appendJsonl(file: string, entry: unknown): Promise<void> {
46
+ await withLock(file, async () => {
47
+ const line = JSON.stringify(entry) + "\n";
48
+ await fs.appendFile(file, line, "utf8");
49
+ });
50
+ }
51
+
52
+ export async function readJsonl<T = unknown>(file: string): Promise<T[]> {
53
+ if (!existsSync(file)) return [];
54
+ const raw = await fs.readFile(file, "utf8");
55
+ const out: T[] = [];
56
+ for (const line of raw.split("\n")) {
57
+ if (!line.trim()) continue;
58
+ try {
59
+ out.push(JSON.parse(line) as T);
60
+ } catch {
61
+ // skip malformed line
62
+ }
63
+ }
64
+ return out;
65
+ }
66
+
67
+ export async function readJson<T>(file: string, fallback: T): Promise<T> {
68
+ if (!existsSync(file)) return fallback;
69
+ try {
70
+ const raw = await fs.readFile(file, "utf8");
71
+ if (!raw.trim()) return fallback;
72
+ return JSON.parse(raw) as T;
73
+ } catch {
74
+ return fallback;
75
+ }
76
+ }
77
+
78
+ export async function writeJson(file: string, data: unknown): Promise<void> {
79
+ await withLock(file, async () => {
80
+ await fs.writeFile(file, JSON.stringify(data, null, 2), "utf8");
81
+ });
82
+ }
83
+
84
+ async function readJsonNoLock<T>(file: string, fallback: T): Promise<T> {
85
+ if (!existsSync(file)) return fallback;
86
+ try {
87
+ const raw = await fs.readFile(file, "utf8");
88
+ if (!raw.trim()) return fallback;
89
+ return JSON.parse(raw) as T;
90
+ } catch {
91
+ return fallback;
92
+ }
93
+ }
94
+
95
+ export async function updateJson<T>(file: string, fallback: T, mutate: (current: T) => T | Promise<T>): Promise<T> {
96
+ return withLock(file, async () => {
97
+ const current = await readJsonNoLock(file, fallback);
98
+ const next = await mutate(current);
99
+ await fs.writeFile(file, JSON.stringify(next, null, 2), "utf8");
100
+ return next;
101
+ });
102
+ }
103
+
104
+ export function inboxFile(agentId: string): string {
105
+ return path.join(INBOX_DIR, `${sanitize(agentId)}.jsonl`);
106
+ }
107
+
108
+ export function cursorFile(agentId: string): string {
109
+ return path.join(CURSOR_DIR, `${sanitize(agentId)}.json`);
110
+ }
111
+
112
+ function sanitize(id: string): string {
113
+ return id.replace(/[^a-zA-Z0-9._-]/g, "_");
114
+ }
115
+
116
+ export async function rewriteJsonl<T>(
117
+ file: string,
118
+ filter: (entry: T) => boolean
119
+ ): Promise<{ kept: number; removed: number }> {
120
+ if (!existsSync(file)) return { kept: 0, removed: 0 };
121
+ return withLock(file, async () => {
122
+ const raw = await fs.readFile(file, "utf8");
123
+ let kept = 0;
124
+ let removed = 0;
125
+ const out: string[] = [];
126
+ for (const line of raw.split("\n")) {
127
+ if (!line.trim()) continue;
128
+ try {
129
+ const entry = JSON.parse(line) as T;
130
+ if (filter(entry)) {
131
+ out.push(line);
132
+ kept++;
133
+ } else {
134
+ removed++;
135
+ }
136
+ } catch {
137
+ removed++;
138
+ }
139
+ }
140
+ await fs.writeFile(file, out.length ? out.join("\n") + "\n" : "", "utf8");
141
+ return { kept, removed };
142
+ });
143
+ }
144
+
145
+ export async function deleteFile(file: string): Promise<boolean> {
146
+ if (!existsSync(file)) return false;
147
+ await fs.unlink(file);
148
+ return true;
149
+ }
150
+
151
+ export async function listInboxFiles(): Promise<string[]> {
152
+ if (!existsSync(INBOX_DIR)) return [];
153
+ const names = await fs.readdir(INBOX_DIR);
154
+ return names.filter((n) => n.endsWith(".jsonl"));
155
+ }
156
+
157
+ export async function fileSize(file: string): Promise<number> {
158
+ if (!existsSync(file)) return 0;
159
+ const st = await fs.stat(file);
160
+ return st.size;
161
+ }
package/src/tools.ts ADDED
@@ -0,0 +1,365 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { watch } from "node:fs";
3
+ import { z } from "zod";
4
+ import path from "node:path";
5
+ import {
6
+ AGENTS_FILE,
7
+ INBOX_DIR,
8
+ ROOM_FILE,
9
+ STATUS_FILE,
10
+ appendJsonl,
11
+ cursorFile,
12
+ deleteFile,
13
+ fileSize,
14
+ inboxFile,
15
+ listInboxFiles,
16
+ readJson,
17
+ readJsonl,
18
+ rewriteJsonl,
19
+ updateJson,
20
+ } from "./store.js";
21
+
22
+ type AgentEntry = {
23
+ agentId: string;
24
+ project?: string;
25
+ role?: string;
26
+ registeredAt: number;
27
+ lastHeartbeat: number;
28
+ };
29
+
30
+ type AgentRegistry = Record<string, AgentEntry>;
31
+
32
+ type Message = {
33
+ id: string;
34
+ ts: number;
35
+ from: string;
36
+ to?: string;
37
+ room?: string;
38
+ text: string;
39
+ };
40
+
41
+ type StatusEntry = {
42
+ id: string;
43
+ ts: number;
44
+ agentId: string;
45
+ status: string;
46
+ detail?: string;
47
+ };
48
+
49
+ type Cursor = {
50
+ inboxOffset?: number;
51
+ roomOffset?: number;
52
+ statusOffset?: number;
53
+ };
54
+
55
+ const STALE_MS = 5 * 60 * 1000;
56
+ const EVICT_MS = 24 * 60 * 60 * 1000;
57
+ const MAX_WAIT_MS = 60_000;
58
+
59
+ // ---------- register ----------
60
+
61
+ export const registerSchema = {
62
+ agentId: z.string().min(1),
63
+ project: z.string().optional(),
64
+ role: z.string().optional(),
65
+ };
66
+
67
+ export async function registerTool(args: { agentId: string; project?: string; role?: string }) {
68
+ const reg = await updateJson<AgentRegistry>(AGENTS_FILE, {}, (current) => {
69
+ const now = Date.now();
70
+ const existing = current[args.agentId];
71
+ current[args.agentId] = {
72
+ agentId: args.agentId,
73
+ project: args.project ?? existing?.project,
74
+ role: args.role ?? existing?.role,
75
+ registeredAt: existing?.registeredAt ?? now,
76
+ lastHeartbeat: now,
77
+ };
78
+ return current;
79
+ });
80
+ return { ok: true, agent: reg[args.agentId] };
81
+ }
82
+
83
+ // ---------- heartbeat ----------
84
+
85
+ export const heartbeatSchema = { agentId: z.string().min(1) };
86
+
87
+ export async function heartbeatTool(args: { agentId: string }) {
88
+ let missing = false;
89
+ await updateJson<AgentRegistry>(AGENTS_FILE, {}, (current) => {
90
+ if (!current[args.agentId]) {
91
+ missing = true;
92
+ return current;
93
+ }
94
+ current[args.agentId].lastHeartbeat = Date.now();
95
+ return current;
96
+ });
97
+ if (missing) return { ok: false, error: `agent '${args.agentId}' not registered` };
98
+ return { ok: true };
99
+ }
100
+
101
+ // ---------- list_agents ----------
102
+
103
+ export const listAgentsSchema = {} as const;
104
+
105
+ export async function listAgentsTool() {
106
+ const now = Date.now();
107
+ const evicted: string[] = [];
108
+ const reg = await updateJson<AgentRegistry>(AGENTS_FILE, {}, (current) => {
109
+ for (const [id, entry] of Object.entries(current)) {
110
+ if (now - entry.lastHeartbeat > EVICT_MS) {
111
+ evicted.push(id);
112
+ delete current[id];
113
+ }
114
+ }
115
+ return current;
116
+ });
117
+ const agents = Object.values(reg).map((a) => ({
118
+ ...a,
119
+ online: now - a.lastHeartbeat < STALE_MS,
120
+ secondsSinceHeartbeat: Math.floor((now - a.lastHeartbeat) / 1000),
121
+ }));
122
+ return { agents, evicted };
123
+ }
124
+
125
+ // ---------- send_message ----------
126
+
127
+ export const sendMessageSchema = {
128
+ from: z.string().min(1),
129
+ to: z.string().optional(),
130
+ room: z.string().optional(),
131
+ text: z.string().min(1),
132
+ };
133
+
134
+ export async function sendMessageTool(args: {
135
+ from: string;
136
+ to?: string;
137
+ room?: string;
138
+ text: string;
139
+ }) {
140
+ const msg: Message = {
141
+ id: randomUUID(),
142
+ ts: Date.now(),
143
+ from: args.from,
144
+ to: args.to,
145
+ room: args.room,
146
+ text: args.text,
147
+ };
148
+ const target = args.to ? inboxFile(args.to) : ROOM_FILE;
149
+ await appendJsonl(target, msg);
150
+ return { ok: true, id: msg.id, target };
151
+ }
152
+
153
+ // ---------- read_messages ----------
154
+
155
+ export const readMessagesSchema = {
156
+ agentId: z.string().min(1),
157
+ source: z.enum(["inbox", "room", "status"]),
158
+ limit: z.number().int().positive().max(500).optional(),
159
+ peek: z.boolean().optional(),
160
+ sinceTs: z.number().optional(),
161
+ };
162
+
163
+ export async function readMessagesTool(args: {
164
+ agentId: string;
165
+ source: "inbox" | "room" | "status";
166
+ limit?: number;
167
+ peek?: boolean;
168
+ sinceTs?: number;
169
+ }) {
170
+ const file = sourceFile(args.source, args.agentId);
171
+ const offsetKey = offsetKeyFor(args.source);
172
+ const all = await readJsonl<Message | StatusEntry>(file);
173
+
174
+ let limited: (Message | StatusEntry)[] = [];
175
+ let totalNew = 0;
176
+
177
+ if (args.peek) {
178
+ const cursor = await readJson<Cursor>(cursorFile(args.agentId), {});
179
+ const startOffset = cursor[offsetKey] ?? 0;
180
+ let entries = all.slice(startOffset);
181
+ if (args.sinceTs !== undefined) entries = entries.filter((e) => e.ts > args.sinceTs!);
182
+ totalNew = entries.length;
183
+ limited = args.limit ? entries.slice(0, args.limit) : entries;
184
+ } else {
185
+ await updateJson<Cursor>(cursorFile(args.agentId), {}, (current) => {
186
+ const startOffset = current[offsetKey] ?? 0;
187
+ let entries = all.slice(startOffset);
188
+ if (args.sinceTs !== undefined) entries = entries.filter((e) => e.ts > args.sinceTs!);
189
+ totalNew = entries.length;
190
+ limited = args.limit ? entries.slice(0, args.limit) : entries;
191
+ if (limited.length > 0) current[offsetKey] = startOffset + limited.length;
192
+ return current;
193
+ });
194
+ }
195
+
196
+ return { messages: limited, totalNew, returned: limited.length };
197
+ }
198
+
199
+ // ---------- post_status ----------
200
+
201
+ export const postStatusSchema = {
202
+ agentId: z.string().min(1),
203
+ status: z.string().min(1),
204
+ detail: z.string().optional(),
205
+ };
206
+
207
+ export async function postStatusTool(args: { agentId: string; status: string; detail?: string }) {
208
+ const entry: StatusEntry = {
209
+ id: randomUUID(),
210
+ ts: Date.now(),
211
+ agentId: args.agentId,
212
+ status: args.status,
213
+ detail: args.detail,
214
+ };
215
+ await appendJsonl(STATUS_FILE, entry);
216
+ return { ok: true, id: entry.id };
217
+ }
218
+
219
+ // ---------- wait_for_message ----------
220
+
221
+ export const waitForMessageSchema = {
222
+ agentId: z.string().min(1),
223
+ source: z.enum(["inbox", "room", "status"]),
224
+ timeoutMs: z.number().int().positive().max(MAX_WAIT_MS).optional(),
225
+ };
226
+
227
+ export async function waitForMessageTool(args: {
228
+ agentId: string;
229
+ source: "inbox" | "room" | "status";
230
+ timeoutMs?: number;
231
+ }) {
232
+ const timeout = Math.min(args.timeoutMs ?? 30_000, MAX_WAIT_MS);
233
+ const file = sourceFile(args.source, args.agentId);
234
+ const startSize = await fileSize(file);
235
+
236
+ const result = await new Promise<{ changed: boolean }>((resolve) => {
237
+ let settled = false;
238
+ const finish = (changed: boolean) => {
239
+ if (settled) return;
240
+ settled = true;
241
+ clearInterval(poll);
242
+ try {
243
+ watcher?.close();
244
+ } catch {
245
+ // ignore
246
+ }
247
+ clearTimeout(t);
248
+ resolve({ changed });
249
+ };
250
+
251
+ const check = async () => {
252
+ const sz = await fileSize(file);
253
+ if (sz > startSize) finish(true);
254
+ };
255
+
256
+ let watcher: ReturnType<typeof watch> | undefined;
257
+ try {
258
+ watcher = watch(file, () => {
259
+ void check();
260
+ });
261
+ } catch {
262
+ // file may not exist; polling will handle
263
+ }
264
+ const poll = setInterval(() => void check(), 500);
265
+ const t = setTimeout(() => finish(false), timeout);
266
+ });
267
+
268
+ if (!result.changed) {
269
+ return { ok: false, timedOut: true };
270
+ }
271
+ return readMessagesTool({ agentId: args.agentId, source: args.source });
272
+ }
273
+
274
+ // ---------- prune ----------
275
+
276
+ export const pruneSchema = {
277
+ olderThanDays: z.number().positive().max(365).optional(),
278
+ removeOrphanInboxes: z.boolean().optional(),
279
+ dryRun: z.boolean().optional(),
280
+ };
281
+
282
+ export async function pruneTool(args: {
283
+ olderThanDays?: number;
284
+ removeOrphanInboxes?: boolean;
285
+ dryRun?: boolean;
286
+ }) {
287
+ const days = args.olderThanDays ?? 7;
288
+ const cutoff = Date.now() - days * 24 * 60 * 60 * 1000;
289
+ const dryRun = args.dryRun ?? false;
290
+
291
+ const reg = await readJson<AgentRegistry>(AGENTS_FILE, {});
292
+ const knownAgents = new Set(Object.keys(reg));
293
+
294
+ if (dryRun) {
295
+ const room = await readJsonl<Message>(ROOM_FILE);
296
+ const status = await readJsonl<StatusEntry>(STATUS_FILE);
297
+ const inboxFiles = await listInboxFiles();
298
+ let inboxRemoved = 0;
299
+ const orphans: string[] = [];
300
+ for (const fname of inboxFiles) {
301
+ const id = fname.replace(/\.jsonl$/, "");
302
+ if (!knownAgents.has(id) && (args.removeOrphanInboxes ?? true)) orphans.push(fname);
303
+ const entries = await readJsonl<Message>(path.join(INBOX_DIR, fname));
304
+ inboxRemoved += entries.filter((e) => e.ts <= cutoff).length;
305
+ }
306
+ return {
307
+ dryRun: true,
308
+ cutoff,
309
+ olderThanDays: days,
310
+ wouldRemove: {
311
+ roomMessages: room.filter((e) => e.ts <= cutoff).length,
312
+ statusEntries: status.filter((e) => e.ts <= cutoff).length,
313
+ inboxMessages: inboxRemoved,
314
+ orphanInboxes: orphans,
315
+ },
316
+ };
317
+ }
318
+
319
+ const roomResult = await rewriteJsonl<Message>(ROOM_FILE, (e) => e.ts > cutoff);
320
+ const statusResult = await rewriteJsonl<StatusEntry>(STATUS_FILE, (e) => e.ts > cutoff);
321
+
322
+ const inboxFiles = await listInboxFiles();
323
+ let inboxRemoved = 0;
324
+ const deletedOrphans: string[] = [];
325
+ for (const fname of inboxFiles) {
326
+ const id = fname.replace(/\.jsonl$/, "");
327
+ const filePath = path.join(INBOX_DIR, fname);
328
+ if (!knownAgents.has(id) && (args.removeOrphanInboxes ?? true)) {
329
+ const entries = await readJsonl<Message>(filePath);
330
+ inboxRemoved += entries.length;
331
+ await deleteFile(filePath);
332
+ deletedOrphans.push(id);
333
+ continue;
334
+ }
335
+ const r = await rewriteJsonl<Message>(filePath, (e) => e.ts > cutoff);
336
+ inboxRemoved += r.removed;
337
+ }
338
+
339
+ return {
340
+ dryRun: false,
341
+ cutoff,
342
+ olderThanDays: days,
343
+ removed: {
344
+ roomMessages: roomResult.removed,
345
+ statusEntries: statusResult.removed,
346
+ inboxMessages: inboxRemoved,
347
+ orphanInboxes: deletedOrphans,
348
+ },
349
+ note: "Cursors not adjusted; clients may see stale offsets but read_messages clamps via slice().",
350
+ };
351
+ }
352
+
353
+ // ---------- helpers ----------
354
+
355
+ function sourceFile(source: "inbox" | "room" | "status", agentId: string): string {
356
+ if (source === "inbox") return inboxFile(agentId);
357
+ if (source === "room") return ROOM_FILE;
358
+ return STATUS_FILE;
359
+ }
360
+
361
+ function offsetKeyFor(source: "inbox" | "room" | "status"): keyof Cursor {
362
+ if (source === "inbox") return "inboxOffset";
363
+ if (source === "room") return "roomOffset";
364
+ return "statusOffset";
365
+ }