@finchagentic/mcp 4.2.0 → 4.5.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.
@@ -134,6 +134,21 @@ exports.STAKE_TOOLS = [
134
134
  required: ["stakeId", "confirm"],
135
135
  },
136
136
  },
137
+ {
138
+ name: "claim_vested_rewards",
139
+ description: "Claim all your fully-vested USDG staking rewards to your custodial wallet. Daily rewards split " +
140
+ "50% instant / 50% vested over 3 days - this claims whatever portion has finished vesting across " +
141
+ "ALL your stakes in one call (check stake_finch_status for accrued totals; you'll also get a " +
142
+ "notification when rewards become claimable). Does nothing to the stake itself - principal stays " +
143
+ "staked and earning. Requires `finch login` and `confirm: true`.",
144
+ inputSchema: {
145
+ type: "object",
146
+ properties: {
147
+ confirm: { type: "boolean", description: "Must be true to claim - this moves real USDG into your custodial wallet." },
148
+ },
149
+ required: ["confirm"],
150
+ },
151
+ },
137
152
  {
138
153
  name: "stake_auto_restake",
139
154
  description: "Turn auto-restake on/off for one of your stakes. When ON, the moment the lock-up ends the stake " +
@@ -143,7 +158,7 @@ exports.STAKE_TOOLS = [
143
158
  "the stake is unlockable and it just sits there earning nothing until you run unstake_finch. " +
144
159
  "Does NOT auto-compound rewards into the stake - that would require signing from your custodial " +
145
160
  "wallet unattended, which this intentionally does not do; claim rewards yourself with " +
146
- "claimVestedRewards (you'll get a notification when they're ready). Requires `finch login`.",
161
+ "claim_vested_rewards (you'll get a notification when they're ready). Requires `finch login`.",
147
162
  inputSchema: {
148
163
  type: "object",
149
164
  properties: {
@@ -246,7 +261,7 @@ async function handleStakeTool(name, args) {
246
261
  isError: true,
247
262
  };
248
263
  }
249
- const result = await (0, convex_js_1.callConvex)("/mcp/stake/stake", "POST", { amountWei: amountWei.toString(), lockTier }, "stake_finch");
264
+ const result = await (0, convex_js_1.callConvex)("/mcp/stake/stake", "POST", { amountWei: amountWei.toString(), lockTier }, "stake_finch", 30000, true);
250
265
  if (result.error)
251
266
  return { content: [{ type: "text", text: `Stake failed: ${result.error}` }], isError: true };
252
267
  const tier = STAKING_TIERS[lockTier ?? 7];
@@ -294,7 +309,7 @@ async function handleStakeTool(name, args) {
294
309
  }
295
310
  const lines = [];
296
311
  for (const id of targetIds) {
297
- const result = await (0, convex_js_1.callConvex)("/mcp/stake/unstake", "POST", { stakeId: id }, "unstake_finch");
312
+ const result = await (0, convex_js_1.callConvex)("/mcp/stake/unstake", "POST", { stakeId: id }, "unstake_finch", 30000, true);
298
313
  if (result.error) {
299
314
  lines.push(`🔴 \`${id}\`: ${result.error}`);
300
315
  }
@@ -304,6 +319,31 @@ async function handleStakeTool(name, args) {
304
319
  }
305
320
  return { content: [{ type: "text", text: lines.join("\n") }] };
306
321
  }
322
+ if (name === "claim_vested_rewards") {
323
+ const { confirm } = (args ?? {});
324
+ if (confirm !== true) {
325
+ return {
326
+ content: [{ type: "text", text: "Refusing to claim without confirmation - pass `confirm: true`." }],
327
+ isError: true,
328
+ };
329
+ }
330
+ const login = requireLogin();
331
+ if (!login)
332
+ return { content: [{ type: "text", text: NOT_LOGGED_IN_MSG }], isError: true };
333
+ const result = await (0, convex_js_1.callConvex)("/mcp/stake/claim-rewards", "POST", {}, "claim_vested_rewards", 30000, true);
334
+ if (result.error)
335
+ return { content: [{ type: "text", text: `Claim failed: ${result.error}` }], isError: true };
336
+ return {
337
+ content: [{
338
+ type: "text",
339
+ text: [
340
+ `✅ Claimed ${(result.claimedAmount ?? 0).toFixed(4)} USDG vested rewards`,
341
+ `Tx: \`${result.txHash}\``,
342
+ `${rh_mcp_js_1.RH_EXPLORER}/tx/${result.txHash}`,
343
+ ].join("\n"),
344
+ }],
345
+ };
346
+ }
307
347
  if (name === "stake_auto_restake") {
308
348
  const { stakeId, enabled } = (args ?? {});
309
349
  if (!stakeId)
@@ -9,14 +9,16 @@ const convex_js_1 = require("../convex.js");
9
9
  const memory_js_1 = require("./memory.js");
10
10
  const local_vault_js_1 = require("../local-vault.js");
11
11
  const local_memory_js_1 = require("../local-memory.js");
12
- const VAULT_TYPES = ["research", "execution", "workflow", "prompt", "file", "memory", "credential"];
12
+ const project_js_1 = require("../project.js");
13
+ const VAULT_TYPES = ["research", "execution", "workflow", "prompt", "file", "memory", "code", "credential"];
13
14
  exports.VAULT_TOOLS = [
14
15
  {
15
16
  name: "vault_save",
16
17
  description: "Save or update a versioned artifact in Finch Vault. Same key = update (git-style: prior version snapshotted, patched to v+1). " +
17
- "Types: research | execution | workflow | prompt | file | memory. " +
18
+ "Types: research | execution | workflow | prompt | file | memory | code. " +
18
19
  "Entries up to 10MB - content over 600KB auto-offloads to blob storage. " +
19
- "For quick unstructured notes, use memory_add instead.",
20
+ "For quick unstructured notes, use memory_add instead. For coding sessions specifically, " +
21
+ "prefer code_session_save - same versioning, but a structured template and auto-linking built in.",
20
22
  inputSchema: {
21
23
  type: "object",
22
24
  properties: {
@@ -29,10 +31,37 @@ exports.VAULT_TOOLS = [
29
31
  tags: { type: "array", items: { type: "string" }, description: "Tags for filtering and search" },
30
32
  commitMsg: { type: "string", description: "Commit message for this version, e.g. 'initial research', 'refined with on-chain data'" },
31
33
  metadata: { type: "string", description: "Optional JSON string for extra structured fields" },
34
+ workspaceProject: {
35
+ type: "string",
36
+ description: "Optional: file this entry into a named Finch workspace project (the same Projects a user " +
37
+ "organizes their Agents/vault into on the Agents page). Matched case-insensitively by name; " +
38
+ "created automatically if it doesn't exist yet. Not the same thing as a `key` path segment - " +
39
+ "this tags the entry in Finch's own project system. Hosted vault only (no effect in local-vault mode).",
40
+ },
32
41
  },
33
42
  required: ["type", "content"],
34
43
  },
35
44
  },
45
+ {
46
+ name: "code_session_save",
47
+ description: "Persist a coding/debugging session as a versioned Markdown snapshot in Finch Vault, keyed by " +
48
+ "project (`code/<project>`) - so the next session (yours, or another agent's) has real context " +
49
+ "instead of starting cold. Same project = new version, full history kept (git-style, like vault_save). " +
50
+ "Auto-links to related past code and research entries. " +
51
+ "Call this at the end of a substantive coding task - not for every single file read or trivial edit.",
52
+ inputSchema: {
53
+ type: "object",
54
+ properties: {
55
+ project: { type: "string", description: "Project or repo slug, e.g. 'finch-webapp', 'mcp-server'. Becomes the vault key: code/<project>." },
56
+ summary: { type: "string", description: "What was done this session - the task, the approach, the outcome." },
57
+ filesChanged: { type: "array", items: { type: "string" }, description: "Files touched, e.g. ['app/convex/vault.ts', 'app/src/App.tsx']" },
58
+ decisions: { type: "string", description: "Notable decisions or tradeoffs made and why - the part a future session can't re-derive from a diff alone." },
59
+ nextSteps: { type: "string", description: "What's left, or what to pick up next session." },
60
+ tags: { type: "array", items: { type: "string" }, description: "Extra tags for search, e.g. ['bugfix', 'refactor']" },
61
+ },
62
+ required: ["project", "summary"],
63
+ },
64
+ },
36
65
  {
37
66
  name: "vault_read",
38
67
  description: "Read a Finch Vault entry by its key. Returns full content, version, tags, and any linked entries.",
@@ -232,6 +261,14 @@ exports.VAULT_TOOLS = [
232
261
  required: ["key"],
233
262
  },
234
263
  },
264
+ {
265
+ name: "list_projects",
266
+ description: "List your Finch workspace projects - the same Projects used to organize Agents and vault content on the " +
267
+ "webapp Agents page. Read-only. Check this before passing `workspaceProject` to vault_save/agent_spawn if " +
268
+ "you want to reuse an existing project rather than relying on the automatic case-insensitive name match. " +
269
+ "No effect / nothing to list in local-vault mode.",
270
+ inputSchema: { type: "object", properties: {}, required: [] },
271
+ },
235
272
  ];
236
273
  // ─── Zod schemas ─────────────────────────────────────────────────────────────
237
274
  const SaveSchema = zod_1.z.object({
@@ -244,6 +281,15 @@ const SaveSchema = zod_1.z.object({
244
281
  tags: zod_1.z.array(zod_1.z.string()).optional(),
245
282
  commitMsg: zod_1.z.string().optional(),
246
283
  metadata: zod_1.z.string().optional(),
284
+ workspaceProject: zod_1.z.string().optional(),
285
+ });
286
+ const CodeSessionSchema = zod_1.z.object({
287
+ project: zod_1.z.string().min(1).max(80),
288
+ summary: zod_1.z.string().min(1),
289
+ filesChanged: zod_1.z.array(zod_1.z.string()).max(100).optional(),
290
+ decisions: zod_1.z.string().optional(),
291
+ nextSteps: zod_1.z.string().optional(),
292
+ tags: zod_1.z.array(zod_1.z.string()).optional(),
247
293
  });
248
294
  const ReadSchema = zod_1.z.object({ key: zod_1.z.string().min(1) });
249
295
  const ListSchema = zod_1.z.object({
@@ -322,7 +368,7 @@ async function handleVaultTool(name, args) {
322
368
  case "vault_save": {
323
369
  const parsed = SaveSchema.safeParse(args);
324
370
  if (!parsed.success)
325
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
371
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
326
372
  if (parsed.data.type === "credential") {
327
373
  // vault_save writes plaintext to disk/DB - "credential" is only a
328
374
  // valid FILTER value for vault_list/search/export (which correctly
@@ -340,7 +386,19 @@ async function handleVaultTool(name, args) {
340
386
  // Auto-generate title from content if not provided
341
387
  const firstLine = parsed.data.content.split("\n")[0].replace(/^#+\s*/, "").slice(0, 80);
342
388
  const autoTitle = parsed.data.title ?? (firstLine || `${parsed.data.type} - ${new Date().toISOString().slice(0, 10)}`);
343
- const savePayload = { ...parsed.data, title: autoTitle };
389
+ const { workspaceProject, ...rest } = parsed.data;
390
+ const savePayload = { ...rest, title: autoTitle };
391
+ // Resolve the project name -> id server-side (auto-creates on first use).
392
+ // Local vault has no project concept at all - workspaceProject is
393
+ // silently a no-op there rather than a confusing network error.
394
+ let resolvedProjectName = null;
395
+ if (workspaceProject && !localVault) {
396
+ const resolved = await (0, project_js_1.resolveProjectId)(workspaceProject);
397
+ if (resolved) {
398
+ savePayload.projectId = resolved.projectId;
399
+ resolvedProjectName = resolved.name;
400
+ }
401
+ }
344
402
  const data = localVault
345
403
  ? (0, local_vault_js_1.localVaultSave)(localVault, savePayload)
346
404
  : await (0, convex_js_1.callConvex)("/vault/save", "POST", savePayload, "vault_save");
@@ -380,6 +438,7 @@ async function handleVaultTool(name, args) {
380
438
  `Key: \`${key}\``,
381
439
  `Version: v${version}`,
382
440
  changed && version > 1 ? `Previous version auto-snapshotted.` : "",
441
+ resolvedProjectName ? `📁 Project: ${resolvedProjectName}` : "",
383
442
  mirrorToMemory ? `🧠 Synced to searchable memory` : (localVault ? `💾 Stored locally at ~/.finch/vault` : ""),
384
443
  ...linkSummary,
385
444
  ``,
@@ -387,10 +446,99 @@ async function handleVaultTool(name, args) {
387
446
  ].filter(Boolean);
388
447
  return { content: [{ type: "text", text: lines.join("\n") }] };
389
448
  }
449
+ case "code_session_save": {
450
+ const parsed = CodeSessionSchema.safeParse(args);
451
+ if (!parsed.success)
452
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
453
+ const { project, summary, filesChanged, decisions, nextSteps, tags } = parsed.data;
454
+ const projectSlug = project.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60) || "session";
455
+ const key = `code/${projectSlug}`;
456
+ const content = [
457
+ `# Code session: ${project}`,
458
+ ``,
459
+ `_${new Date().toISOString()}_`,
460
+ ``,
461
+ `## Summary`,
462
+ summary,
463
+ filesChanged?.length ? `\n## Files changed\n${filesChanged.map((f) => `- \`${f}\``).join("\n")}` : "",
464
+ decisions ? `\n## Decisions\n${decisions}` : "",
465
+ nextSteps ? `\n## Next steps\n${nextSteps}` : "",
466
+ ].filter(Boolean).join("\n");
467
+ const savePayload = {
468
+ type: "code",
469
+ key,
470
+ title: `Code: ${project}`,
471
+ content,
472
+ contentType: "markdown",
473
+ agentId: "code-session",
474
+ tags: ["code-session", ...(tags ?? [])],
475
+ commitMsg: summary.slice(0, 80),
476
+ };
477
+ const data = localVault
478
+ ? (0, local_vault_js_1.localVaultSave)(localVault, savePayload)
479
+ : await (0, convex_js_1.callConvex)("/vault/save", "POST", savePayload, "vault_save");
480
+ if (data.error)
481
+ return { content: [{ type: "text", text: `Error: ${data.error}` }], isError: true };
482
+ const { version, changed } = data;
483
+ // Same mirror-to-memory rule as vault_save: skip only when local vault
484
+ // is on WITHOUT local memory, so query terms never phone home for a
485
+ // fully offline setup.
486
+ const mirrorToMemory = !localVault || !!(0, local_memory_js_1.getLocalMemoryConfig)();
487
+ if (mirrorToMemory) {
488
+ (0, memory_js_1.syncToSupermemory)(content, {
489
+ vaultKey: key, title: savePayload.title, type: "code",
490
+ tags: savePayload.tags, version, source: "code_session_save",
491
+ });
492
+ }
493
+ // Auto-link to related past code/research entries - same idea as
494
+ // deep_research's auto-linking, so a project's session history and any
495
+ // research that informed it stay connected instead of sitting as
496
+ // disconnected entries. Purely additive - never blocks the save.
497
+ const linked = [];
498
+ try {
499
+ const searchQuery = `${project} ${summary}`.slice(0, 200);
500
+ let hits = [];
501
+ if (localVault) {
502
+ const searchResult = (0, local_vault_js_1.localVaultSearch)(localVault, searchQuery, { limit: 6 });
503
+ hits = (searchResult.results ?? [])
504
+ .filter((r) => !!r.key && r.key !== key)
505
+ .map((r) => ({ key: r.key, title: r.title ?? "(untitled)" }));
506
+ }
507
+ else {
508
+ const searchResult = (await (0, convex_js_1.callConvex)("/vault/search", "POST", { q: searchQuery, n: 8 }, "vault_search"));
509
+ hits = (searchResult?.results ?? [])
510
+ .map((r) => {
511
+ const hitKey = r.metadata?.vaultKey ?? r.metadata?.key ?? null;
512
+ return hitKey ? { key: hitKey, title: r.metadata?.title ?? "(untitled)" } : null;
513
+ })
514
+ .filter((h) => !!h && h.key !== key);
515
+ }
516
+ for (const hit of hits.slice(0, 3)) {
517
+ try {
518
+ if (localVault)
519
+ (0, local_vault_js_1.localVaultLink)(localVault, key, hit.key, "related");
520
+ else
521
+ await (0, convex_js_1.callConvex)("/vault/link", "POST", { fromKey: key, toKey: hit.key, relation: "related" }, "vault_link");
522
+ linked.push(hit.key);
523
+ }
524
+ catch { /* skip individual link failures */ }
525
+ }
526
+ }
527
+ catch { /* auto-link is purely additive */ }
528
+ const lines = [
529
+ `📦 **Code session ${changed ? (version === 1 ? "saved" : "updated") : "unchanged"}** - \`${key}\` (v${version})`,
530
+ filesChanged?.length ? `Files: ${filesChanged.length}` : "",
531
+ linked.length ? `🔗 Linked to ${linked.length} related entr${linked.length === 1 ? "y" : "ies"}: ${linked.map((k) => `\`${k}\``).join(", ")}` : "",
532
+ mirrorToMemory ? `🧠 Synced to searchable memory` : "",
533
+ ``,
534
+ `Next session: \`vault_read key="${key}"\` for the latest state, or \`vault_history key="${key}"\` for the full timeline.`,
535
+ ].filter(Boolean);
536
+ return { content: [{ type: "text", text: lines.join("\n") }] };
537
+ }
390
538
  case "vault_read": {
391
539
  const parsed = ReadSchema.safeParse(args);
392
540
  if (!parsed.success)
393
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
541
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
394
542
  const vaultReadKey = parsed.data.key;
395
543
  let data;
396
544
  try {
@@ -440,12 +588,26 @@ async function handleVaultTool(name, args) {
440
588
  const backlinksBlock = Array.isArray(data.backlinks) && data.backlinks.length > 0
441
589
  ? `\n🔙 Linked from (${data.backlinks.length}):\n${data.backlinks.map((b) => ` ← \`${b.key}\`${b.title ? ` - ${b.title}` : ""}`).join("\n")}`
442
590
  : "";
591
+ // A raw projectId means nothing to a reader - resolve it to a name.
592
+ // One extra call only when the entry is actually tagged; skipped
593
+ // entirely for the common case (unassigned) and in local-vault mode.
594
+ let projectLabel = "";
595
+ if (data.projectId && !localVault) {
596
+ try {
597
+ const projData = await (0, convex_js_1.callConvex)("/projects/list", "GET", undefined, "list_projects");
598
+ const match = projData?.projects?.find((p) => p.id === data.projectId);
599
+ if (match)
600
+ projectLabel = `📁 Project: ${match.name}`;
601
+ }
602
+ catch { /* best-effort - never block the read over this */ }
603
+ }
443
604
  const lines = [
444
605
  `📂 **${data.title}**`,
445
606
  `Key: \`${data.key}\` · Type: ${data.type} · v${data.version} · ${sizeLabel}${data.contentFileId ? " · blob" : ""}`,
446
607
  data.tags?.length ? `Tags: ${data.tags.join(", ")}` : "",
447
608
  data.isPinned ? "📌 Pinned" : "",
448
609
  data.agentId ? `Agent: ${data.agentId}` : "",
610
+ projectLabel,
449
611
  `Updated: ${formatDate(data.updatedAt)}`,
450
612
  data.linkedKeys?.length ? `\nLinks out:\n${data.linkedKeys.map((l) => ` → ${l}`).join("\n")}` : "",
451
613
  backlinksBlock,
@@ -459,7 +621,7 @@ async function handleVaultTool(name, args) {
459
621
  case "vault_list": {
460
622
  const parsed = ListSchema.safeParse(args ?? {});
461
623
  if (!parsed.success)
462
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
624
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
463
625
  const params = new URLSearchParams();
464
626
  if (parsed.data.type)
465
627
  params.set("type", parsed.data.type);
@@ -491,7 +653,7 @@ async function handleVaultTool(name, args) {
491
653
  case "vault_search": {
492
654
  const parsed = SearchSchema.safeParse(args);
493
655
  if (!parsed.success)
494
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
656
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
495
657
  // Full-text search, proxied through Convex (searchSupermemory is a
496
658
  // legacy name - it calls Finch's own /memory/search endpoint, not a
497
659
  // third-party semantic service; there is no embedding step here). Large vault
@@ -592,7 +754,7 @@ async function handleVaultTool(name, args) {
592
754
  case "vault_history": {
593
755
  const parsed = HistorySchema.safeParse(args);
594
756
  if (!parsed.success)
595
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
757
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
596
758
  const histKey = parsed.data.key;
597
759
  let data;
598
760
  try {
@@ -632,7 +794,7 @@ async function handleVaultTool(name, args) {
632
794
  case "vault_diff": {
633
795
  const parsed = DiffSchema.safeParse(args);
634
796
  if (!parsed.success)
635
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
797
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
636
798
  const { key, fromVersion, toVersion } = parsed.data;
637
799
  let data;
638
800
  try {
@@ -676,7 +838,7 @@ async function handleVaultTool(name, args) {
676
838
  case "vault_export": {
677
839
  const parsed = ExportSchema.safeParse(args ?? {});
678
840
  if (!parsed.success)
679
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
841
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
680
842
  const params = parsed.data.type ? `?type=${parsed.data.type}` : "";
681
843
  const data = localVault
682
844
  ? (0, local_vault_js_1.localVaultExport)(localVault, parsed.data.type)
@@ -695,7 +857,7 @@ async function handleVaultTool(name, args) {
695
857
  case "vault_store_credential": {
696
858
  const parsed = StoreCredentialSchema.safeParse(args);
697
859
  if (!parsed.success)
698
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
860
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
699
861
  const data = localVault
700
862
  ? (0, local_vault_js_1.localVaultStoreCredential)(localVault, parsed.data.name, parsed.data.value, parsed.data.description)
701
863
  : await (0, convex_js_1.callConvex)("/vault/credential/store", "POST", parsed.data, "vault_store_credential");
@@ -706,7 +868,7 @@ async function handleVaultTool(name, args) {
706
868
  case "vault_get_credential": {
707
869
  const parsed = GetCredentialSchema.safeParse(args);
708
870
  if (!parsed.success)
709
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
871
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
710
872
  const params = new URLSearchParams({ name: parsed.data.name });
711
873
  let data;
712
874
  try {
@@ -729,7 +891,7 @@ async function handleVaultTool(name, args) {
729
891
  case "vault_pin": {
730
892
  const parsed = PinSchema.safeParse(args);
731
893
  if (!parsed.success)
732
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
894
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
733
895
  const { key, pinned = true } = parsed.data;
734
896
  const data = localVault
735
897
  ? (0, local_vault_js_1.localVaultPin)(localVault, key, pinned)
@@ -741,7 +903,7 @@ async function handleVaultTool(name, args) {
741
903
  case "vault_unpublish": {
742
904
  const parsed = UnpublishSchema.safeParse(args);
743
905
  if (!parsed.success)
744
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
906
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
745
907
  // Publishing is a hosted/marketplace concept - a local vault is private
746
908
  // by construction, so there is nothing to retract.
747
909
  if (localVault) {
@@ -762,7 +924,7 @@ async function handleVaultTool(name, args) {
762
924
  case "vault_delete": {
763
925
  const parsed = DeleteSchema.safeParse(args);
764
926
  if (!parsed.success)
765
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
927
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
766
928
  if (args?.confirm !== true) {
767
929
  return {
768
930
  content: [{
@@ -797,7 +959,7 @@ async function handleVaultTool(name, args) {
797
959
  case "vault_tag": {
798
960
  const parsed = TagSchema.safeParse(args);
799
961
  if (!parsed.success)
800
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
962
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
801
963
  const { key, tags, replace = false } = parsed.data;
802
964
  const data = localVault
803
965
  ? (0, local_vault_js_1.localVaultTag)(localVault, key, tags, replace)
@@ -809,7 +971,7 @@ async function handleVaultTool(name, args) {
809
971
  case "vault_link": {
810
972
  const parsed = LinkSchema.safeParse(args);
811
973
  if (!parsed.success)
812
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
974
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
813
975
  const { fromKey, toKey, relation } = parsed.data;
814
976
  const data = localVault
815
977
  ? (0, local_vault_js_1.localVaultLink)(localVault, fromKey, toKey, relation)
@@ -822,7 +984,7 @@ async function handleVaultTool(name, args) {
822
984
  case "vault_related": {
823
985
  const parsed = RelatedSchema.safeParse(args);
824
986
  if (!parsed.success)
825
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
987
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
826
988
  const { key, relation } = parsed.data;
827
989
  const params = new URLSearchParams({ key });
828
990
  if (relation)
@@ -838,6 +1000,20 @@ async function handleVaultTool(name, args) {
838
1000
  const lines = items.map(r => `- **${r.title}** (\`${r.key}\`) [${r.type}] - ${r.direction} \`${r.relation}\``);
839
1001
  return { content: [{ type: "text", text: `## Related entries for \`${key}\` (${items.length})\n\n${lines.join("\n")}` }] };
840
1002
  }
1003
+ case "list_projects": {
1004
+ if (localVault) {
1005
+ return { content: [{ type: "text", text: "Local vault mode has no project concept - `workspaceProject` has no effect, and there's nothing to list here." }] };
1006
+ }
1007
+ const data = await (0, convex_js_1.callConvex)("/projects/list", "GET", undefined, "list_projects");
1008
+ if (data.error)
1009
+ return { content: [{ type: "text", text: `Error: ${data.error}` }], isError: true };
1010
+ const projects = data.projects ?? [];
1011
+ if (!projects.length) {
1012
+ return { content: [{ type: "text", text: "No projects yet. Pass `workspaceProject: \"name\"` to `vault_save` or `agent_spawn` and one is created automatically." }] };
1013
+ }
1014
+ const lines = projects.map((p) => `- **${p.name}** (\`${p.slug}\`)${p.description ? ` - ${p.description}` : ""}`);
1015
+ return { content: [{ type: "text", text: `📁 **Your projects** (${projects.length})\n\n${lines.join("\n")}` }] };
1016
+ }
841
1017
  default:
842
1018
  return null;
843
1019
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@finchagentic/mcp",
3
- "version": "4.2.0",
3
+ "version": "4.5.0",
4
4
  "description": "The runtime layer for Agentic AI. Persistent memory, autonomous agents, and workflows that survive every session.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -50,9 +50,8 @@
50
50
  "LICENSE"
51
51
  ],
52
52
  "dependencies": {
53
- "@modelcontextprotocol/sdk": "^1.0.0",
54
- "ethers": "^6.16.0",
55
- "node-fetch": "^3.3.2",
53
+ "@modelcontextprotocol/sdk": "^1.30.0",
54
+ "ethers": "^6.17.0",
56
55
  "zod": "^4.4.3"
57
56
  },
58
57
  "devDependencies": {