feat(cli): vault set / watch add / webhook create + prune dead MCP stubs
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:
@@ -1,5 +1,36 @@
|
||||
# Changelog
|
||||
|
||||
## 1.23.0 (2026-05-03) — close the CLI surface, prune dead MCP stubs
|
||||
|
||||
Three previously-MCP-only write verbs land on the CLI, closing every
|
||||
functional gap between the (defunct since 1.5.0) MCP tool registry and
|
||||
the CLI:
|
||||
|
||||
- `claudemesh vault set <key> <value>` — encrypts client-side via
|
||||
`crypto_secretbox_easy` with a fresh symmetric key, then seals the
|
||||
key to the member's own pubkey via `crypto_box_seal` (same shape as
|
||||
the file-share crypto). Flags: `--type env|file`, `--mount <path>`,
|
||||
`--description <text>`. Pairs with the existing `vault list/delete`.
|
||||
- `claudemesh watch add <url>` — registers a URL change watcher.
|
||||
Flags: `--label`, `--interval <sec>`, `--mode`, `--extract <css>`,
|
||||
`--notify-on changed|always`. Pairs with `watch list/remove`.
|
||||
- `claudemesh webhook create <name>` — issues a fresh inbound webhook;
|
||||
prints url + one-shot secret. Pairs with `webhook list/delete`.
|
||||
|
||||
Cleanup: removed 22 dead stub files under `apps/cli/src/mcp/tools/*`,
|
||||
the unused `router.ts`, `middleware/*`, and `handlers/*` directories
|
||||
(~120 LoC). The MCP server in 1.5.0+ has been a tool-less push-pipe;
|
||||
these stubs were leftover scaffolding that never wired into the
|
||||
`tools/list` response. The legitimate MCP surfaces stay untouched:
|
||||
|
||||
- `<channel source="claudemesh">` push pipe (the irreducible reason
|
||||
MCP exists at all — no other Claude Code surface can inject events
|
||||
mid-turn).
|
||||
- Mesh skills exposed as MCP **prompts** (slash commands) and
|
||||
**resources** (`skill://claudemesh/<name>`).
|
||||
- Mesh-deployed MCP services proxied via the sub-process tool
|
||||
surface (separate code path under server.ts:855+).
|
||||
|
||||
## 1.22.1 (2026-05-03) — daemon docs + help
|
||||
|
||||
- Root `claudemesh --help` now lists the `daemon` subcommand suite under
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "claudemesh-cli",
|
||||
"version": "1.22.1",
|
||||
"version": "1.23.0",
|
||||
"description": "Peer mesh for Claude Code sessions — CLI + MCP server.",
|
||||
"keywords": [
|
||||
"claude-code",
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
||||
@@ -1 +0,0 @@
|
||||
export { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
@@ -1 +0,0 @@
|
||||
export function formatToolError(err: unknown): string { return err instanceof Error ? err.message : String(err); }
|
||||
@@ -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");
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
// Tool dispatch — server.ts handles all routing via switch statement.
|
||||
export const ROUTER_VERSION = "1.0" as const;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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";
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user