fathom-mcp 0.4.10 → 0.4.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fathom-mcp",
3
- "version": "0.4.10",
3
+ "version": "0.4.12",
4
4
  "description": "MCP server for Fathom — vault operations, search, rooms, and cross-workspace communication",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -129,7 +129,7 @@ function copyScripts(targetDir) {
129
129
  // --- Headless agent integration ----------------------------------------------
130
130
 
131
131
  const HEADLESS_CMDS = {
132
- "claude-code": (prompt) => ["claude", "-p", prompt],
132
+ "claude-code": (prompt) => ["claude", "-p", "--dangerously-skip-permissions", prompt],
133
133
  "codex": (prompt) => ["codex", "exec", prompt],
134
134
  "gemini": (prompt) => ["gemini", prompt],
135
135
  "opencode": (prompt) => ["opencode", "run", prompt],
@@ -411,19 +411,20 @@ async function runInit(flags = {}) {
411
411
  }
412
412
 
413
413
  // 5. Server URL
414
- const serverUrl = nonInteractive
414
+ let serverUrl = nonInteractive
415
415
  ? (flagServer || "http://localhost:4243")
416
416
  : await ask(rl, "\n Fathom server URL", "http://localhost:4243");
417
417
 
418
418
  // 6. API key
419
- const apiKey = flagApiKey || (nonInteractive ? "" : await ask(rl, " API key (from dashboard or server first-run output)", ""));
419
+ let apiKey = flagApiKey || (nonInteractive ? "" : await ask(rl, " API key (from dashboard or server first-run output)", ""));
420
420
 
421
421
  // 7. Server probe — check reachability early
422
- const regClient = createClient({ server: serverUrl, apiKey, workspace });
423
- const serverReachable = serverUrl ? await regClient.healthCheck() : false;
422
+ let regClient = createClient({ server: serverUrl, apiKey, workspace });
423
+ let serverReachable = serverUrl ? await regClient.healthCheck() : false;
424
424
  const serverOnPath = detectFathomServer();
425
425
 
426
- if (!serverReachable) {
426
+ // Retry loop for server connectivity (interactive mode)
427
+ while (!serverReachable && !nonInteractive) {
427
428
  console.log(`\n ⚠ Fathom server not reachable at ${serverUrl}\n`);
428
429
  if (serverOnPath === "installed") {
429
430
  console.log(" Start it: fathom-server");
@@ -433,11 +434,25 @@ async function runInit(flags = {}) {
433
434
  console.log(" # or: docker run -p 4243:4243 ghcr.io/myra/fathom-server");
434
435
  }
435
436
  console.log("\n Without the server, only \"local\" and \"none\" vault modes are available.");
437
+ const retry = await askYesNo(rl, "\n Try a different server URL or API key?", true);
438
+ if (!retry) break;
439
+ serverUrl = await ask(rl, "\n Fathom server URL", serverUrl);
440
+ apiKey = await ask(rl, " API key", apiKey);
441
+ regClient = createClient({ server: serverUrl, apiKey, workspace });
442
+ serverReachable = await regClient.healthCheck();
443
+ if (serverReachable) {
444
+ console.log(" ✓ Server connected!");
445
+ }
436
446
  }
437
447
 
438
448
  // 8. Vault mode selection
439
449
  let vaultMode;
440
450
  if (nonInteractive) {
451
+ if (!serverReachable && flagServer) {
452
+ console.error(`\n Error: Server at ${serverUrl} is not reachable.`);
453
+ console.error(" Fix the URL or start the server, then re-run init.");
454
+ process.exit(1);
455
+ }
441
456
  vaultMode = serverReachable ? "hosted" : "local";
442
457
  console.log(` Vault mode: ${vaultMode} (auto-selected)`);
443
458
  } else {
@@ -664,12 +679,13 @@ async function runInit(flags = {}) {
664
679
  } else {
665
680
  if (cmdParts) {
666
681
  const [cmd, ...args] = cmdParts;
667
- const displayCmd = `${cmd} ${args[0]}${args.length > 1 ? " ..." : ""}`;
682
+ const flagArgs = args.slice(0, -1).join(" ");
683
+ const displayCmd = `${cmd} ${flagArgs} <prompt>`;
668
684
  const rl2 = readline.createInterface({ input: process.stdin, output: process.stdout });
669
685
  console.log("\n" + "─".repeat(60));
670
686
  const integrate = await askYesNo(
671
687
  rl2,
672
- `\n Auto-integrate instructions into your project?\n This will run: ${displayCmd}\n\n Proceed?`,
688
+ `\n Auto-integrate instructions into your project?\n This will run: ${displayCmd}\n\n ⚠ This uses --dangerously-skip-permissions so the agent can\n write to CLAUDE.md without prompting. If you prefer, decline\n and we'll print the instructions for you to add manually.\n\n Proceed?`,
673
689
  true,
674
690
  );
675
691
  rl2.close();
package/src/index.js CHANGED
@@ -209,8 +209,8 @@ const tools = [
209
209
  "Post a message to a shared room. Rooms are created implicitly on first post. " +
210
210
  "Use this for ambient, multilateral communication — unlike fathom_send (point-to-point DM), " +
211
211
  "room messages are visible to all participants. Responding is optional — use `<...>` for active silence. " +
212
- "Supports @workspace mentions (e.g. @fathom, @navier-stokes) — mentioned workspaces get the message " +
213
- "injected into their Claude session, same mechanism as fathom_send. Use @all to notify every workspace except sender.",
212
+ "Supports @workspace mentions (e.g. @fathom, @navier-stokes) — mentioned workspaces receive " +
213
+ "notifications in their mentions:{workspace} virtual room via fathom_room_list. Use @all to notify every workspace except sender.",
214
214
  inputSchema: {
215
215
  type: "object",
216
216
  properties: {
@@ -227,7 +227,9 @@ const tools = [
227
227
  "to the latest message. Default: 60 minutes before the latest message. Use start to look " +
228
228
  "further back. Example: minutes=15, start=120 returns 15 minutes of conversation starting " +
229
229
  "2 hours before the latest message. Response includes window metadata with has_older flag " +
230
- "for pseudo-pagination. Automatically marks the room as read for this workspace unless mark_read=false.",
230
+ "for pseudo-pagination. For virtual rooms (dm:*, mentions:*), messages are consumed on read " +
231
+ "deleted after being returned. Use mark_read=false to peek without consuming. " +
232
+ "For regular rooms, automatically marks as read unless mark_read=false.",
231
233
  inputSchema: {
232
234
  type: "object",
233
235
  properties: {
@@ -244,7 +246,8 @@ const tools = [
244
246
  description:
245
247
  "List all rooms with activity summary — message count, last activity time, last sender, " +
246
248
  "description, and per-room unread_count for this workspace. Use to discover active rooms " +
247
- "and see which have new messages.",
249
+ "and see which have new messages. Virtual rooms (dm:{workspace}, mentions:{workspace}) " +
250
+ "appear only for the owning workspace and disappear when empty.",
248
251
  inputSchema: {
249
252
  type: "object",
250
253
  properties: {},
@@ -279,9 +282,10 @@ const tools = [
279
282
  {
280
283
  name: "fathom_send",
281
284
  description:
282
- "Send a message to another workspace's agent instance — for cross-workspace coordination, " +
283
- "sharing findings, or requesting action. Use fathom_workspaces first to discover valid " +
284
- "targets. The target agent sees: 'Message from workspace ({from}): {message}'",
285
+ "Send a message to another workspace's agent instance — stored in their dm:{workspace} " +
286
+ "virtual room. Use fathom_workspaces first to discover valid targets. Recipients see DMs " +
287
+ "via fathom_room_list and consume them with fathom_room_read. " +
288
+ "Message format: 'Message from workspace ({from}): {message}'",
285
289
  inputSchema: {
286
290
  type: "object",
287
291
  properties: {
@@ -624,6 +628,16 @@ async function main() {
624
628
  // Startup sync for synced mode (fire-and-forget)
625
629
  startupSync().catch(() => {});
626
630
 
631
+ // Heartbeat — report liveness to server every 30s
632
+ if (config.server && config.workspace) {
633
+ const beat = () =>
634
+ client
635
+ .heartbeat(config.workspace, config.agents?.[0], config.vaultMode)
636
+ .catch(() => {});
637
+ beat(); // immediate
638
+ setInterval(beat, 30_000);
639
+ }
640
+
627
641
  const transport = new StdioServerTransport();
628
642
  await server.connect(transport);
629
643
  }
@@ -205,6 +205,15 @@ export function createClient(config) {
205
205
  });
206
206
  }
207
207
 
208
+ // --- Heartbeat -------------------------------------------------------------
209
+
210
+ async function heartbeat(ws, agent, vaultMode) {
211
+ const body = {};
212
+ if (agent) body.agent = agent;
213
+ if (vaultMode) body.vault_mode = vaultMode;
214
+ return request("POST", `/api/workspaces/${encodeURIComponent(ws)}/heartbeat`, { body });
215
+ }
216
+
208
217
  // --- Auth ------------------------------------------------------------------
209
218
 
210
219
  async function getApiKey() {
@@ -245,6 +254,7 @@ export function createClient(config) {
245
254
  pushFile,
246
255
  syncManifest,
247
256
  createRoutine,
257
+ heartbeat,
248
258
  getApiKey,
249
259
  healthCheck,
250
260
  };