feat(cli): vault set / watch add / webhook create + prune dead MCP stubs
Some checks failed
CI / Lint (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled

Closes the last functional gaps where the MCP tool registry exposed
write verbs the CLI didn't:

- vault set <k> <v> [--type env|file --mount <path> --description ...]
  Client-side crypto_secretbox_easy with a fresh symmetric key sealed
  to the member's own pubkey via crypto_box_seal — same pattern used
  for file shares. Pairs with the existing vault list/delete.
- watch add <url> [--label --interval --mode --extract --notify-on]
  Pairs with watch list/remove.
- webhook create <name> — pairs with webhook list/delete.

Cleanup: deletes 22 stub files under apps/cli/src/mcp/tools/* plus
router.ts, middleware/, handlers/ (~120 LoC). These were FAMILY/TOOLS
metadata-only re-exports left over from before the 1.5.0 tool-less
push-pipe flip; nothing imports them. The legitimate MCP surfaces
stay: the inbound <channel> push pipe, mesh skills as prompts and
skill:// resources, and the mesh-service proxy mode.

Released as 1.23.0 on npm.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alejandro Gutiérrez
2026-05-03 20:53:25 +01:00
parent 4eff4f5a20
commit c56910bfcf
31 changed files with 160 additions and 120 deletions

View File

@@ -348,6 +348,52 @@ export async function runVaultDelete(key: string, opts: Flags): Promise<number>
});
}
export interface VaultSetOpts extends Flags {
entryType?: "env" | "file";
mountPath?: string;
description?: string;
}
export async function runVaultSet(key: string, value: string, opts: VaultSetOpts): Promise<number> {
if (!key || value == null) {
render.err("Usage: claudemesh vault set <key> <value> [--type env|file] [--mount /path] [--description ...]");
return EXIT.INVALID_ARGS;
}
const { encryptFile, sealKeyForPeer } = await import("~/services/crypto/file-crypto.js");
const { getMeshConfig } = await import("~/services/config/facade.js");
const { readConfig } = await import("~/services/config/facade.js");
const config = readConfig();
const slug = opts.mesh ?? (config.meshes.length === 1 ? config.meshes[0]!.slug : null);
if (!slug) {
render.err("multiple meshes joined; pass --mesh <slug>");
return EXIT.INVALID_ARGS;
}
const mesh = getMeshConfig(slug);
if (!mesh) { render.err(`not joined to mesh "${slug}"`); return EXIT.NOT_FOUND; }
const plaintext = new TextEncoder().encode(value);
const enc = await encryptFile(plaintext);
const ciphertextB64 = Buffer.from(enc.ciphertext).toString("base64");
const sealed = await sealKeyForPeer(enc.key, mesh.pubkey);
return await withMesh({ meshSlug: slug }, async (client) => {
const ok = await client.vaultSet(
key,
ciphertextB64,
enc.nonce,
sealed,
opts.entryType ?? "env",
opts.mountPath,
opts.description,
);
if (opts.json) emitJson({ key, stored: ok });
else if (ok) render.ok(`vault[${bold(key)}] stored`, dim(`(${ciphertextB64.length}b)`));
else render.err(`vault set failed for "${key}"`);
return ok ? EXIT.SUCCESS : EXIT.IO_ERROR;
});
}
// ════════════════════════════════════════════════════════════════════════
// watch — URL change watchers
// ════════════════════════════════════════════════════════════════════════
@@ -368,6 +414,39 @@ export async function runWatchList(opts: Flags): Promise<number> {
});
}
export interface WatchAddOpts extends Flags {
label?: string;
interval?: number;
mode?: string;
extract?: string;
notifyOn?: string;
}
export async function runWatchAdd(url: string, opts: WatchAddOpts): Promise<number> {
if (!url) {
render.err("Usage: claudemesh watch add <url> [--label ...] [--interval <sec>] [--extract <css>] [--notify-on changed|always]");
return EXIT.INVALID_ARGS;
}
return await withMesh({ meshSlug: opts.mesh ?? null }, async (client) => {
const result = await client.watch(url, {
label: opts.label,
interval: opts.interval,
mode: opts.mode,
extract: opts.extract,
notify_on: opts.notifyOn,
});
if (result?.error) {
if (opts.json) emitJson({ ok: false, error: result.error });
else render.err(`watch add failed: ${result.error}`);
return EXIT.IO_ERROR;
}
const id = String((result as any)?.id ?? (result as any)?.watch_id ?? "?");
if (opts.json) emitJson({ ok: true, id, url, ...(opts.label ? { label: opts.label } : {}) });
else render.ok(`watching ${clay(url)}`, dim(id.slice(0, 8)));
return EXIT.SUCCESS;
});
}
export async function runUnwatch(id: string, opts: Flags): Promise<number> {
if (!id) { render.err("Usage: claudemesh watch remove <id>"); return EXIT.INVALID_ARGS; }
return await withMesh({ meshSlug: opts.mesh ?? null }, async (client) => {
@@ -397,6 +476,28 @@ export async function runWebhookList(opts: Flags): Promise<number> {
});
}
export async function runWebhookCreate(name: string, opts: Flags): Promise<number> {
if (!name) {
render.err("Usage: claudemesh webhook create <name>");
return EXIT.INVALID_ARGS;
}
return await withMesh({ meshSlug: opts.mesh ?? null }, async (client) => {
const created = await client.createWebhook(name);
if (!created) {
if (opts.json) emitJson({ ok: false, error: "create failed (timeout or duplicate)" });
else render.err(`webhook create "${name}" failed`);
return EXIT.IO_ERROR;
}
if (opts.json) emitJson({ ok: true, ...created });
else {
render.ok(`created webhook ${bold(created.name)}`);
process.stdout.write(` url: ${clay(created.url)}\n`);
process.stdout.write(` secret: ${dim(created.secret)} ${dim("(shown once)")}\n`);
}
return EXIT.SUCCESS;
});
}
export async function runWebhookDelete(name: string, opts: Flags): Promise<number> {
if (!name) { render.err("Usage: claudemesh webhook delete <name>"); return EXIT.INVALID_ARGS; }
return await withMesh({ meshSlug: opts.mesh ?? null }, async (client) => {

View File

@@ -156,9 +156,9 @@ Platform
claudemesh stream create|publish|list pub/sub event bus
claudemesh sql query|execute|schema per-mesh SQL
claudemesh skill list|get|remove mesh-published skills
claudemesh vault list|delete encrypted secrets
claudemesh watch list|remove URL change watchers
claudemesh webhook list|delete outbound HTTP triggers
claudemesh vault set|list|delete encrypted secrets (set: --type env|file --mount /p)
claudemesh watch add|list|remove URL change watchers (add: --label --interval --extract)
claudemesh webhook create|list|delete outbound HTTP triggers
claudemesh file share <path> [--to peer] upload (or local-host fast path if --to matches)
claudemesh file get <id> [--out path] download by id
claudemesh file list|status|delete shared mesh files
@@ -591,7 +591,16 @@ async function main(): Promise<void> {
const f = { mesh: flags.mesh as string, json: !!flags.json };
if (sub === "list") { const { runVaultList } = await import("~/commands/platform-actions.js"); process.exit(await runVaultList(f)); }
else if (sub === "delete") { const { runVaultDelete } = await import("~/commands/platform-actions.js"); process.exit(await runVaultDelete(positionals[1] ?? "", f)); }
else { console.error("Usage: claudemesh vault <list|delete> (set/get currently via MCP — needs crypto)"); process.exit(EXIT.INVALID_ARGS); }
else if (sub === "set") {
const { runVaultSet } = await import("~/commands/platform-actions.js");
process.exit(await runVaultSet(positionals[1] ?? "", positionals[2] ?? "", {
...f,
entryType: (flags.type as "env" | "file" | undefined),
mountPath: flags.mount as string | undefined,
description: flags.description as string | undefined,
}));
}
else { console.error("Usage: claudemesh vault <list|set|delete>"); process.exit(EXIT.INVALID_ARGS); }
break;
}
case "watch": {
@@ -599,7 +608,18 @@ async function main(): Promise<void> {
const f = { mesh: flags.mesh as string, json: !!flags.json };
if (sub === "list") { const { runWatchList } = await import("~/commands/platform-actions.js"); process.exit(await runWatchList(f)); }
else if (sub === "remove") { const { runUnwatch } = await import("~/commands/platform-actions.js"); process.exit(await runUnwatch(positionals[1] ?? "", f)); }
else { console.error("Usage: claudemesh watch <list|remove>"); process.exit(EXIT.INVALID_ARGS); }
else if (sub === "add") {
const { runWatchAdd } = await import("~/commands/platform-actions.js");
process.exit(await runWatchAdd(positionals[1] ?? "", {
...f,
label: flags.label as string | undefined,
interval: flags.interval ? Number(flags.interval) : undefined,
mode: flags.mode as string | undefined,
extract: flags.extract as string | undefined,
notifyOn: flags["notify-on"] as string | undefined,
}));
}
else { console.error("Usage: claudemesh watch <list|add|remove>"); process.exit(EXIT.INVALID_ARGS); }
break;
}
case "webhook": {
@@ -607,7 +627,8 @@ async function main(): Promise<void> {
const f = { mesh: flags.mesh as string, json: !!flags.json };
if (sub === "list") { const { runWebhookList } = await import("~/commands/platform-actions.js"); process.exit(await runWebhookList(f)); }
else if (sub === "delete") { const { runWebhookDelete } = await import("~/commands/platform-actions.js"); process.exit(await runWebhookDelete(positionals[1] ?? "", f)); }
else { console.error("Usage: claudemesh webhook <list|delete>"); process.exit(EXIT.INVALID_ARGS); }
else if (sub === "create") { const { runWebhookCreate } = await import("~/commands/platform-actions.js"); process.exit(await runWebhookCreate(positionals[1] ?? "", f)); }
else { console.error("Usage: claudemesh webhook <list|create|delete>"); process.exit(EXIT.INVALID_ARGS); }
break;
}
case "file": {

View File

@@ -1 +0,0 @@
export { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";

View File

@@ -1 +0,0 @@
export { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";

View File

@@ -1 +0,0 @@
export function formatToolError(err: unknown): string { return err instanceof Error ? err.message : String(err); }

View File

@@ -1,3 +0,0 @@
export function logToolCall(toolName: string, durationMs: number): void {
if (process.env.CLAUDEMESH_DEBUG === "1") process.stderr.write("[mcp] " + toolName + " (" + durationMs + "ms)\n");
}

View File

@@ -1,2 +0,0 @@
// Tool dispatch — server.ts handles all routing via switch statement.
export const ROUTER_VERSION = "1.0" as const;

View File

@@ -1,4 +0,0 @@
// MCP tool family: clock-write
// Handlers in mcp/server.ts; this file defines the family for the spec's folder structure.
export const FAMILY = "clock-write" as const;
export const TOOLS = ["mesh_set_clock", "mesh_pause_clock", "mesh_resume_clock"] as const;

View File

@@ -1,4 +0,0 @@
// MCP tool family: contexts
// Handlers in mcp/server.ts; this file defines the family for the spec's folder structure.
export const FAMILY = "contexts" as const;
export const TOOLS = ["share_context", "get_context", "list_contexts"] as const;

View File

@@ -1,4 +0,0 @@
// MCP tool family: files
// Handlers in mcp/server.ts; this file defines the family for the spec's folder structure.
export const FAMILY = "files" as const;
export const TOOLS = ["share_file", "get_file", "list_files", "file_status", "delete_file", "grant_file_access", "read_peer_file", "list_peer_files"] as const;

View File

@@ -1,4 +0,0 @@
// MCP tool family: graph
// Handlers in mcp/server.ts; this file defines the family for the spec's folder structure.
export const FAMILY = "graph" as const;
export const TOOLS = ["graph_query", "graph_execute"] as const;

View File

@@ -1,4 +0,0 @@
// MCP tool family: groups
// Handlers in mcp/server.ts; this file defines the family for the spec's folder structure.
export const FAMILY = "groups" as const;
export const TOOLS = ["join_group", "leave_group"] as const;

View File

@@ -1,21 +0,0 @@
export { FAMILY as memoryFamily, TOOLS as memoryTools } from "./memory.js";
export { FAMILY as stateFamily, TOOLS as stateTools } from "./state.js";
export { FAMILY as messagingFamily, TOOLS as messagingTools } from "./messaging.js";
export { FAMILY as profileFamily, TOOLS as profileTools } from "./profile.js";
export { FAMILY as groupsFamily, TOOLS as groupsTools } from "./groups.js";
export { FAMILY as filesFamily, TOOLS as filesTools } from "./files.js";
export { FAMILY as vectorsFamily, TOOLS as vectorsTools } from "./vectors.js";
export { FAMILY as graphFamily, TOOLS as graphTools } from "./graph.js";
export { FAMILY as sqlFamily, TOOLS as sqlTools } from "./sql.js";
export { FAMILY as streamsFamily, TOOLS as streamsTools } from "./streams.js";
export { FAMILY as contextsFamily, TOOLS as contextsTools } from "./contexts.js";
export { FAMILY as tasksFamily, TOOLS as tasksTools } from "./tasks.js";
export { FAMILY as schedulingFamily, TOOLS as schedulingTools } from "./scheduling.js";
export { FAMILY as meshMetaFamily, TOOLS as meshMetaTools } from "./mesh-meta.js";
export { FAMILY as clockWriteFamily, TOOLS as clockWriteTools } from "./clock-write.js";
export { FAMILY as skillsFamily, TOOLS as skillsTools } from "./skills.js";
export { FAMILY as mcpRegistryPeerFamily, TOOLS as mcpRegistryPeerTools } from "./mcp-registry-peer.js";
export { FAMILY as mcpRegistryBrokerFamily, TOOLS as mcpRegistryBrokerTools } from "./mcp-registry-broker.js";
export { FAMILY as vaultFamily, TOOLS as vaultTools } from "./vault.js";
export { FAMILY as urlWatchFamily, TOOLS as urlWatchTools } from "./url-watch.js";
export { FAMILY as webhooksFamily, TOOLS as webhooksTools } from "./webhooks.js";

View File

@@ -1,4 +0,0 @@
// MCP tool family: mcp-registry-broker
// Handlers in mcp/server.ts; this file defines the family for the spec's folder structure.
export const FAMILY = "mcp-registry-broker" as const;
export const TOOLS = ["mesh_mcp_deploy", "mesh_mcp_undeploy", "mesh_mcp_update", "mesh_mcp_logs", "mesh_mcp_scope", "mesh_mcp_schema", "mesh_mcp_catalog"] as const;

View File

@@ -1,4 +0,0 @@
// MCP tool family: mcp-registry-peer
// Handlers in mcp/server.ts; this file defines the family for the spec's folder structure.
export const FAMILY = "mcp-registry-peer" as const;
export const TOOLS = ["mesh_mcp_register", "mesh_mcp_list", "mesh_tool_call", "mesh_mcp_remove"] as const;

View File

@@ -1,4 +0,0 @@
// MCP tool family: memory
// Handlers in mcp/server.ts; this file defines the family for the spec's folder structure.
export const FAMILY = "memory" as const;
export const TOOLS = ["remember", "recall", "forget"] as const;

View File

@@ -1,4 +0,0 @@
// MCP tool family: mesh-meta
// Handlers in mcp/server.ts; this file defines the family for the spec's folder structure.
export const FAMILY = "mesh-meta" as const;
export const TOOLS = ["mesh_info", "mesh_stats", "mesh_clock", "ping_mesh"] as const;

View File

@@ -1,4 +0,0 @@
// MCP tool family: messaging
// Handlers in mcp/server.ts; this file defines the family for the spec's folder structure.
export const FAMILY = "messaging" as const;
export const TOOLS = ["send_message", "list_peers", "check_messages", "message_status"] as const;

View File

@@ -1,4 +0,0 @@
// MCP tool family: profile
// Handlers in mcp/server.ts; this file defines the family for the spec's folder structure.
export const FAMILY = "profile" as const;
export const TOOLS = ["set_profile", "set_status", "set_summary", "set_visible"] as const;

View File

@@ -1,4 +0,0 @@
// MCP tool family: scheduling
// Handlers in mcp/server.ts; this file defines the family for the spec's folder structure.
export const FAMILY = "scheduling" as const;
export const TOOLS = ["schedule_reminder", "list_scheduled", "cancel_scheduled"] as const;

View File

@@ -1,4 +0,0 @@
// MCP tool family: skills
// Handlers in mcp/server.ts; this file defines the family for the spec's folder structure.
export const FAMILY = "skills" as const;
export const TOOLS = ["share_skill", "get_skill", "list_skills", "remove_skill", "mesh_skill_deploy"] as const;

View File

@@ -1,4 +0,0 @@
// MCP tool family: sql
// Handlers in mcp/server.ts; this file defines the family for the spec's folder structure.
export const FAMILY = "sql" as const;
export const TOOLS = ["mesh_query", "mesh_execute", "mesh_schema"] as const;

View File

@@ -1,4 +0,0 @@
// MCP tool family: state
// Handlers in mcp/server.ts; this file defines the family for the spec's folder structure.
export const FAMILY = "state" as const;
export const TOOLS = ["set_state", "get_state", "list_state"] as const;

View File

@@ -1,4 +0,0 @@
// MCP tool family: streams
// Handlers in mcp/server.ts; this file defines the family for the spec's folder structure.
export const FAMILY = "streams" as const;
export const TOOLS = ["create_stream", "publish", "subscribe", "list_streams"] as const;

View File

@@ -1,4 +0,0 @@
// MCP tool family: tasks
// Handlers in mcp/server.ts; this file defines the family for the spec's folder structure.
export const FAMILY = "tasks" as const;
export const TOOLS = ["create_task", "claim_task", "complete_task", "list_tasks"] as const;

View File

@@ -1,4 +0,0 @@
// MCP tool family: url-watch
// Handlers in mcp/server.ts; this file defines the family for the spec's folder structure.
export const FAMILY = "url-watch" as const;
export const TOOLS = ["mesh_watch", "mesh_unwatch", "mesh_watches"] as const;

View File

@@ -1,4 +0,0 @@
// MCP tool family: vault
// Handlers in mcp/server.ts; this file defines the family for the spec's folder structure.
export const FAMILY = "vault" as const;
export const TOOLS = ["vault_set", "vault_list", "vault_delete"] as const;

View File

@@ -1,4 +0,0 @@
// MCP tool family: vectors
// Handlers in mcp/server.ts; this file defines the family for the spec's folder structure.
export const FAMILY = "vectors" as const;
export const TOOLS = ["vector_store", "vector_search", "vector_delete", "list_collections"] as const;

View File

@@ -1,4 +0,0 @@
// MCP tool family: webhooks
// Handlers in mcp/server.ts; this file defines the family for the spec's folder structure.
export const FAMILY = "webhooks" as const;
export const TOOLS = ["create_webhook", "list_webhooks", "delete_webhook"] as const;