fathom-mcp 0.5.12 → 0.5.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -2
- package/src/agents.js +1 -1
- package/src/cli.js +20 -61
- package/src/config.js +1 -1
- package/src/index.js +75 -1
- package/src/server-client.js +14 -0
- package/src/ws-connection.js +17 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fathom-mcp",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.14",
|
|
4
4
|
"description": "MCP server for Fathom — vault operations, search, rooms, and cross-workspace communication",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
"main": "src/index.js",
|
|
10
10
|
"files": [
|
|
11
11
|
"src/",
|
|
12
|
-
"scripts
|
|
12
|
+
"scripts/*.sh",
|
|
13
|
+
"scripts/*.py",
|
|
13
14
|
"fathom-agents.md",
|
|
14
15
|
"README.md",
|
|
15
16
|
"CHANGELOG.md",
|
package/src/agents.js
CHANGED
|
@@ -9,7 +9,7 @@ import fs from "fs";
|
|
|
9
9
|
import path from "path";
|
|
10
10
|
import { execSync, execFileSync } from "child_process";
|
|
11
11
|
|
|
12
|
-
const CONFIG_DIR = path.join(process.env.HOME || "/tmp", ".config", "fathom-mcp");
|
|
12
|
+
const CONFIG_DIR = process.env.FATHOM_CONFIG_DIR || path.join(process.env.HOME || "/tmp", ".config", "fathom-mcp");
|
|
13
13
|
const AGENTS_FILE = path.join(CONFIG_DIR, "agents.json");
|
|
14
14
|
|
|
15
15
|
const EMPTY_CONFIG = { version: 1, agents: {} };
|
package/src/cli.js
CHANGED
|
@@ -149,9 +149,7 @@ function copyScripts(targetDir) {
|
|
|
149
149
|
const HEADLESS_CMDS = {
|
|
150
150
|
"claude-code": (prompt) => ["claude", "-p", "--dangerously-skip-permissions", prompt],
|
|
151
151
|
"claude-sdk": (prompt) => ["claude", "-p", "--dangerously-skip-permissions", prompt],
|
|
152
|
-
"codex": (prompt) => ["codex", "exec", prompt],
|
|
153
152
|
"gemini": (prompt) => ["gemini", prompt],
|
|
154
|
-
"opencode": (prompt) => ["opencode", "run", prompt],
|
|
155
153
|
};
|
|
156
154
|
|
|
157
155
|
function buildIntegrationPrompt(blob) {
|
|
@@ -249,27 +247,6 @@ function writeMcpJson(cwd) {
|
|
|
249
247
|
return ".mcp.json";
|
|
250
248
|
}
|
|
251
249
|
|
|
252
|
-
function writeCodexToml(cwd) {
|
|
253
|
-
const dir = path.join(cwd, ".codex");
|
|
254
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
255
|
-
const filePath = path.join(dir, "config.toml");
|
|
256
|
-
|
|
257
|
-
let content = "";
|
|
258
|
-
try {
|
|
259
|
-
content = fs.readFileSync(filePath, "utf-8");
|
|
260
|
-
} catch { /* file doesn't exist */ }
|
|
261
|
-
|
|
262
|
-
// Check if fathom-vault section already exists
|
|
263
|
-
if (/\[mcp_servers\.fathom-vault\]/.test(content)) {
|
|
264
|
-
return ".codex/config.toml (already configured)";
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
const section = `\n[mcp_servers.fathom-vault]\ncommand = "npx"\nargs = ["-y", "fathom-mcp"]\n`;
|
|
268
|
-
const separator = content && !content.endsWith("\n") ? "\n" : "";
|
|
269
|
-
fs.writeFileSync(filePath, content + separator + section);
|
|
270
|
-
return ".codex/config.toml";
|
|
271
|
-
}
|
|
272
|
-
|
|
273
250
|
function writeGeminiJson(cwd) {
|
|
274
251
|
const dir = path.join(cwd, ".gemini");
|
|
275
252
|
fs.mkdirSync(dir, { recursive: true });
|
|
@@ -280,22 +257,6 @@ function writeGeminiJson(cwd) {
|
|
|
280
257
|
return ".gemini/settings.json";
|
|
281
258
|
}
|
|
282
259
|
|
|
283
|
-
function writeOpencodeJson(cwd) {
|
|
284
|
-
const filePath = path.join(cwd, "opencode.json");
|
|
285
|
-
const existing = readJsonFile(filePath) || {};
|
|
286
|
-
deepMerge(existing, {
|
|
287
|
-
mcp: {
|
|
288
|
-
"fathom-vault": {
|
|
289
|
-
type: "local",
|
|
290
|
-
command: ["npx", "-y", "fathom-mcp"],
|
|
291
|
-
enabled: true,
|
|
292
|
-
},
|
|
293
|
-
},
|
|
294
|
-
});
|
|
295
|
-
writeJsonFile(filePath, existing);
|
|
296
|
-
return "opencode.json";
|
|
297
|
-
}
|
|
298
|
-
|
|
299
260
|
const AGENTS = {
|
|
300
261
|
"claude-code": {
|
|
301
262
|
name: "Claude Code",
|
|
@@ -304,38 +265,31 @@ const AGENTS = {
|
|
|
304
265
|
hasHooks: true,
|
|
305
266
|
nextSteps: 'Add to CLAUDE.md: `ToolSearch query="+fathom" max_results=20`',
|
|
306
267
|
},
|
|
307
|
-
"
|
|
308
|
-
name: "
|
|
309
|
-
detect: (cwd) => fs.existsSync(path.join(cwd, ".
|
|
310
|
-
configWriter:
|
|
311
|
-
hasHooks:
|
|
312
|
-
nextSteps: "
|
|
268
|
+
"claude-sdk": {
|
|
269
|
+
name: "Claude SDK (headless)",
|
|
270
|
+
detect: (cwd) => fs.existsSync(path.join(cwd, ".claude")),
|
|
271
|
+
configWriter: writeMcpJson,
|
|
272
|
+
hasHooks: true,
|
|
273
|
+
nextSteps: "Headless Claude Code — no TUI, structured I/O. Start with: npx fathom-mcp start",
|
|
313
274
|
},
|
|
314
275
|
"gemini": {
|
|
315
276
|
name: "Gemini CLI",
|
|
316
277
|
detect: (cwd) => fs.existsSync(path.join(cwd, ".gemini")),
|
|
317
278
|
configWriter: writeGeminiJson,
|
|
318
|
-
hasHooks:
|
|
279
|
+
hasHooks: true,
|
|
319
280
|
nextSteps: "Run `gemini` in this directory — fathom tools load automatically.",
|
|
320
281
|
},
|
|
321
|
-
"
|
|
322
|
-
name: "
|
|
323
|
-
detect: (
|
|
324
|
-
configWriter:
|
|
282
|
+
"manual": {
|
|
283
|
+
name: "I'll set up my agent myself",
|
|
284
|
+
detect: () => false,
|
|
285
|
+
configWriter: () => "(skipped — manual setup)",
|
|
325
286
|
hasHooks: false,
|
|
326
|
-
nextSteps: "
|
|
327
|
-
},
|
|
328
|
-
"claude-sdk": {
|
|
329
|
-
name: "Claude SDK (headless)",
|
|
330
|
-
detect: (cwd) => fs.existsSync(path.join(cwd, ".claude")),
|
|
331
|
-
configWriter: writeMcpJson,
|
|
332
|
-
hasHooks: true,
|
|
333
|
-
nextSteps: "Headless Claude Code — no TUI, structured I/O. Start with: npx fathom-mcp start",
|
|
287
|
+
nextSteps: "Point your agent's MCP config at: npx -y fathom-mcp",
|
|
334
288
|
},
|
|
335
289
|
};
|
|
336
290
|
|
|
337
291
|
// Exported for testing
|
|
338
|
-
export { AGENTS, writeMcpJson,
|
|
292
|
+
export { AGENTS, writeMcpJson, writeGeminiJson };
|
|
339
293
|
|
|
340
294
|
// --- Init wizard -------------------------------------------------------------
|
|
341
295
|
|
|
@@ -404,7 +358,7 @@ async function runInit(flags = {}) {
|
|
|
404
358
|
const agent = AGENTS[key];
|
|
405
359
|
const isDetected = detected.includes(key);
|
|
406
360
|
const mark = isDetected ? "✓" : " ";
|
|
407
|
-
const markers = { "claude-code": ".claude/", "
|
|
361
|
+
const markers = { "claude-code": ".claude/", "claude-sdk": ".claude/", "gemini": ".gemini/" };
|
|
408
362
|
const hint = isDetected ? ` (${markers[key] || key} found)` : "";
|
|
409
363
|
console.log(` ${mark} ${agent.name}${hint}`);
|
|
410
364
|
}
|
|
@@ -630,6 +584,11 @@ async function runInit(flags = {}) {
|
|
|
630
584
|
}
|
|
631
585
|
}
|
|
632
586
|
|
|
587
|
+
// Register in CLI agent registry (for ls/start/stop)
|
|
588
|
+
const entry = buildEntryFromConfig(cwd, configData);
|
|
589
|
+
registryAddAgent(workspace, entry);
|
|
590
|
+
console.log(` ✓ Registered agent "${workspace}" in CLI registry`);
|
|
591
|
+
|
|
633
592
|
// Context-aware next steps
|
|
634
593
|
console.log(`\n Done! Fathom MCP is configured for workspace "${workspace}".`);
|
|
635
594
|
console.log(` Vault mode: ${vaultMode}`);
|
|
@@ -1000,7 +959,7 @@ async function runAdd(argv) {
|
|
|
1000
959
|
|
|
1001
960
|
const name = await ask(rl, " Agent name", nameArg || defaultName);
|
|
1002
961
|
const agentProjectDir = await ask(rl, " Project directory", defaults.projectDir);
|
|
1003
|
-
const agentType = await ask(rl, " Agent type (claude-code|claude-sdk|
|
|
962
|
+
const agentType = await ask(rl, " Agent type (claude-code|claude-sdk|gemini|manual)", defaults.agentType);
|
|
1004
963
|
const executionMode = await ask(rl, " Execution mode (tmux|headless)", defaults.executionMode);
|
|
1005
964
|
const command = await ask(rl, " Command", defaultCommand(agentType, executionMode));
|
|
1006
965
|
const server = await ask(rl, " Server URL", defaults.server);
|
package/src/config.js
CHANGED
package/src/index.js
CHANGED
|
@@ -346,6 +346,18 @@ const tools = [
|
|
|
346
346
|
required: [],
|
|
347
347
|
},
|
|
348
348
|
},
|
|
349
|
+
{
|
|
350
|
+
name: "fathom_key_rotate",
|
|
351
|
+
description:
|
|
352
|
+
"Rotate this agent's API key. Revokes the current per-agent key and issues a new one. " +
|
|
353
|
+
"Updates the local agents.json config and reconnects the WebSocket. " +
|
|
354
|
+
"Only works with per-agent keys — admin keys use the dashboard.",
|
|
355
|
+
inputSchema: {
|
|
356
|
+
type: "object",
|
|
357
|
+
properties: {},
|
|
358
|
+
required: [],
|
|
359
|
+
},
|
|
360
|
+
},
|
|
349
361
|
];
|
|
350
362
|
|
|
351
363
|
// --- Vault routing by mode ---------------------------------------------------
|
|
@@ -471,6 +483,26 @@ const telegramTools = [
|
|
|
471
483
|
},
|
|
472
484
|
];
|
|
473
485
|
|
|
486
|
+
// --- Primary-agent-only tools (policy gate) ----------------------------------
|
|
487
|
+
|
|
488
|
+
const primaryAgentTools = [
|
|
489
|
+
{
|
|
490
|
+
name: "fathom_session_inject",
|
|
491
|
+
description:
|
|
492
|
+
"Inject a keystroke into a workspace's tmux session. Primary agent only. " +
|
|
493
|
+
"Used by the policy gate to respond to permission prompts. " +
|
|
494
|
+
"Keys must be a single digit 1-9 (to select a numbered option) or a named key (Enter, Escape).",
|
|
495
|
+
inputSchema: {
|
|
496
|
+
type: "object",
|
|
497
|
+
properties: {
|
|
498
|
+
workspace: { type: "string", description: "Target workspace name (e.g. 'navier-stokes')" },
|
|
499
|
+
keys: { type: "string", description: "Keys to send — single digit 1-9 or named key (Enter, Escape)" },
|
|
500
|
+
},
|
|
501
|
+
required: ["workspace", "keys"],
|
|
502
|
+
},
|
|
503
|
+
},
|
|
504
|
+
];
|
|
505
|
+
|
|
474
506
|
// --- Server setup & dispatch -------------------------------------------------
|
|
475
507
|
|
|
476
508
|
const server = new Server(
|
|
@@ -489,7 +521,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
489
521
|
} catch {
|
|
490
522
|
// If settings unavailable, hide telegram tools
|
|
491
523
|
}
|
|
492
|
-
const allTools = [...tools, ...(showTelegram ? telegramTools : [])];
|
|
524
|
+
const allTools = [...tools, ...(showTelegram ? [...telegramTools, ...primaryAgentTools] : [])];
|
|
493
525
|
return { tools: allTools };
|
|
494
526
|
});
|
|
495
527
|
|
|
@@ -657,6 +689,41 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
657
689
|
break;
|
|
658
690
|
}
|
|
659
691
|
|
|
692
|
+
// --- Key rotation ---
|
|
693
|
+
case "fathom_key_rotate": {
|
|
694
|
+
const rotateResult = await client.rotateKey();
|
|
695
|
+
if (rotateResult.error) {
|
|
696
|
+
result = { error: `Key rotation failed: ${rotateResult.error}` };
|
|
697
|
+
break;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// Update agents.json with new key
|
|
701
|
+
try {
|
|
702
|
+
const { loadAgentsConfig, saveAgentsConfig } = await import("./agents.js");
|
|
703
|
+
const agentsConf = loadAgentsConfig();
|
|
704
|
+
if (agentsConf.agents[config.workspace]) {
|
|
705
|
+
agentsConf.agents[config.workspace].apiKey = rotateResult.api_key;
|
|
706
|
+
saveAgentsConfig(agentsConf);
|
|
707
|
+
}
|
|
708
|
+
} catch {
|
|
709
|
+
// agents.json update failed — still return the new key
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// Reconnect WebSocket with new key
|
|
713
|
+
if (wsConn) {
|
|
714
|
+
wsConn.close();
|
|
715
|
+
config.apiKey = rotateResult.api_key;
|
|
716
|
+
wsConn = createWSConnection(config);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
result = {
|
|
720
|
+
ok: true,
|
|
721
|
+
workspace: rotateResult.workspace,
|
|
722
|
+
message: "Key rotated successfully. agents.json updated, WebSocket reconnected.",
|
|
723
|
+
};
|
|
724
|
+
break;
|
|
725
|
+
}
|
|
726
|
+
|
|
660
727
|
// --- Telegram ---
|
|
661
728
|
case "fathom_telegram_contacts":
|
|
662
729
|
result = await client.telegramContacts(config.workspace);
|
|
@@ -750,6 +817,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
750
817
|
}
|
|
751
818
|
break;
|
|
752
819
|
}
|
|
820
|
+
// --- Session injection (policy gate) ---
|
|
821
|
+
case "fathom_session_inject": {
|
|
822
|
+
if (!args.workspace) { result = { error: "workspace is required" }; break; }
|
|
823
|
+
if (!args.keys) { result = { error: "keys is required" }; break; }
|
|
824
|
+
result = await client.injectKeys(args.workspace, args.keys);
|
|
825
|
+
break;
|
|
826
|
+
}
|
|
753
827
|
case "fathom_telegram_send_voice": {
|
|
754
828
|
const voiceContactArg = args.contact;
|
|
755
829
|
if (!voiceContactArg) { result = { error: "contact is required" }; break; }
|
package/src/server-client.js
CHANGED
|
@@ -298,6 +298,14 @@ export function createClient(config) {
|
|
|
298
298
|
});
|
|
299
299
|
}
|
|
300
300
|
|
|
301
|
+
// --- Session injection (policy gate) ----------------------------------------
|
|
302
|
+
|
|
303
|
+
async function injectKeys(targetWorkspace, keys) {
|
|
304
|
+
return request("POST", `/api/session/${encodeURIComponent(targetWorkspace)}/inject`, {
|
|
305
|
+
body: { keys },
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
301
309
|
// --- Settings --------------------------------------------------------------
|
|
302
310
|
|
|
303
311
|
async function getSettings() {
|
|
@@ -310,6 +318,10 @@ export function createClient(config) {
|
|
|
310
318
|
return request("GET", "/api/auth/key");
|
|
311
319
|
}
|
|
312
320
|
|
|
321
|
+
async function rotateKey() {
|
|
322
|
+
return request("POST", "/api/auth/keys/rotate");
|
|
323
|
+
}
|
|
324
|
+
|
|
313
325
|
async function healthCheck() {
|
|
314
326
|
try {
|
|
315
327
|
const resp = await fetch(`${baseUrl}/api/auth/status`, {
|
|
@@ -357,8 +369,10 @@ export function createClient(config) {
|
|
|
357
369
|
telegramSendVoice,
|
|
358
370
|
telegramStatus,
|
|
359
371
|
speak,
|
|
372
|
+
injectKeys,
|
|
360
373
|
getSettings,
|
|
361
374
|
getApiKey,
|
|
375
|
+
rotateKey,
|
|
362
376
|
healthCheck,
|
|
363
377
|
};
|
|
364
378
|
}
|
package/src/ws-connection.js
CHANGED
|
@@ -54,14 +54,20 @@ export function createWSConnection(config) {
|
|
|
54
54
|
function connect() {
|
|
55
55
|
if (closed) return;
|
|
56
56
|
|
|
57
|
+
// Redact token from URL for logging
|
|
58
|
+
const redactedUrl = wsUrl.replace(/token=[^&]+/, "token=***");
|
|
59
|
+
console.error(`[ws] connecting to ${redactedUrl}`);
|
|
60
|
+
|
|
57
61
|
try {
|
|
58
62
|
ws = new WebSocket(wsUrl);
|
|
59
|
-
} catch {
|
|
63
|
+
} catch (err) {
|
|
64
|
+
console.error(`[ws] connection constructor failed: ${err.message}`);
|
|
60
65
|
scheduleReconnect();
|
|
61
66
|
return;
|
|
62
67
|
}
|
|
63
68
|
|
|
64
69
|
ws.on("open", () => {
|
|
70
|
+
console.error(`[ws] connected — sending hello (agent=${agent}, vault_mode=${vaultMode})`);
|
|
65
71
|
reconnectDelay = INITIAL_RECONNECT_MS;
|
|
66
72
|
|
|
67
73
|
// Send hello handshake
|
|
@@ -85,10 +91,12 @@ export function createWSConnection(config) {
|
|
|
85
91
|
|
|
86
92
|
switch (msg.type) {
|
|
87
93
|
case "welcome":
|
|
94
|
+
console.error(`[ws] welcome received — connection established for workspace=${workspace}`);
|
|
88
95
|
break;
|
|
89
96
|
|
|
90
97
|
case "inject":
|
|
91
98
|
case "ping_fire":
|
|
99
|
+
console.error(`[ws] received ${msg.type} (${(msg.text || "").length} chars)`);
|
|
92
100
|
injectMessage(msg.text || "");
|
|
93
101
|
break;
|
|
94
102
|
|
|
@@ -101,18 +109,21 @@ export function createWSConnection(config) {
|
|
|
101
109
|
break;
|
|
102
110
|
|
|
103
111
|
case "error":
|
|
112
|
+
console.error(`[ws] server error: ${msg.message || JSON.stringify(msg)}`);
|
|
104
113
|
// Server rejected us — don't reconnect immediately
|
|
105
114
|
reconnectDelay = MAX_RECONNECT_MS;
|
|
106
115
|
break;
|
|
107
116
|
}
|
|
108
117
|
});
|
|
109
118
|
|
|
110
|
-
ws.on("close", () => {
|
|
119
|
+
ws.on("close", (code, reason) => {
|
|
120
|
+
console.error(`[ws] closed (code=${code}, reason=${reason || "none"})`);
|
|
111
121
|
stopKeepalive();
|
|
112
122
|
if (!closed) scheduleReconnect();
|
|
113
123
|
});
|
|
114
124
|
|
|
115
|
-
ws.on("error", () => {
|
|
125
|
+
ws.on("error", (err) => {
|
|
126
|
+
console.error(`[ws] error: ${err.message}`);
|
|
116
127
|
// Error always followed by close event — reconnect handled there
|
|
117
128
|
stopKeepalive();
|
|
118
129
|
});
|
|
@@ -144,6 +155,7 @@ export function createWSConnection(config) {
|
|
|
144
155
|
|
|
145
156
|
function scheduleReconnect() {
|
|
146
157
|
if (closed) return;
|
|
158
|
+
console.error(`[ws] reconnecting in ${reconnectDelay}ms`);
|
|
147
159
|
setTimeout(connect, reconnectDelay);
|
|
148
160
|
reconnectDelay = Math.min(reconnectDelay * 2, MAX_RECONNECT_MS);
|
|
149
161
|
}
|
|
@@ -177,8 +189,8 @@ export function createWSConnection(config) {
|
|
|
177
189
|
timeout: 5000,
|
|
178
190
|
stdio: "ignore",
|
|
179
191
|
});
|
|
180
|
-
} catch {
|
|
181
|
-
|
|
192
|
+
} catch (err) {
|
|
193
|
+
console.error(`[ws] tmux inject failed for ${pane}: ${err.message}`);
|
|
182
194
|
}
|
|
183
195
|
}
|
|
184
196
|
|