refactor: rename cli-v2 → cli, archive legacy cli, plus broker-side grants + auto-migrate
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

- apps/cli/ is now the canonical CLI (was apps/cli-v2/).
- apps/cli/ legacy v0 archived as branch 'legacy-cli-archive' and tag
  'cli-v0-legacy-final' before deletion; git history preserves it too.
- .github/workflows/release-cli.yml paths updated.
- pnpm-lock.yaml regenerated.

Broker-side peer-grant enforcement (spec: 2026-04-15-per-peer-capabilities):
- 0020_peer-grants.sql adds peer_grants jsonb + GIN index on mesh.member.
- handleSend in broker fetches recipient grant maps once per send, drops
  messages silently when sender lacks the required capability.
- POST /cli/mesh/:slug/grants to update from CLI; broker_messages_dropped_by_grant_total metric.
- CLI grant/revoke/block now mirror to broker via syncToBroker.

Auto-migrate on broker startup:
- apps/broker/src/migrate.ts runs drizzle migrate with pg_advisory_lock
  before the HTTP server binds. Exits non-zero on failure so Coolify
  healthcheck fails closed.
- Dockerfile copies packages/db/migrations into /app/migrations.
- postgres 3.4.5 added as direct broker dep.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alejandro Gutiérrez
2026-04-15 08:44:52 +01:00
parent c9ede3d469
commit ee12510ef1
374 changed files with 14706 additions and 11307 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -15,9 +15,10 @@ import {
ListResourcesRequestSchema,
ReadResourceRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { TOOLS } from "./tools";
import { loadConfig } from "../state/config";
import { startClients, stopAll, findClient, allClients } from "../ws/manager";
import { TOOLS } from "./tools/definitions.js";
import { readConfig } from "~/services/config/facade.js";
import { BrokerClient, startClients, stopAll, findClient, allClients } from "~/services/broker/facade.js";
import type { InboundPush } from "~/services/broker/facade.js";
import type {
Priority,
PeerStatus,
@@ -25,9 +26,7 @@ import type {
SetStatusArgs,
SetSummaryArgs,
ListPeersArgs,
} from "./types";
import { BrokerClient } from "../ws/client";
import type { InboundPush } from "../ws/client";
} from "./types.js";
/** Compute a human-readable relative time string from an ISO timestamp. */
function relativeTime(isoStr: string): string {
@@ -105,6 +104,7 @@ async function resolveClient(to: string): Promise<{
p.displayName.toLowerCase().includes(nameLower),
);
if (partials.length === 1) {
process.stderr.write(`[claudemesh] resolved "${target}" → "${partials[0]!.displayName}" (partial match)\n`);
return { client: c, targetSpec: partials[0]!.pubkey };
}
}
@@ -155,7 +155,7 @@ export async function startMcpServer(): Promise<void> {
return startServiceProxy(process.argv[serviceIdx + 1]!);
}
const config = loadConfig();
const config = readConfig();
const myName = config.displayName ?? "unnamed";
const myRole = config.role ?? process.env.CLAUDEMESH_ROLE ?? null;
@@ -433,7 +433,7 @@ Your message mode is "${messageMode}".
switch (name) {
case "send_message": {
const { to, message, priority } = (args ?? {}) as SendMessageArgs;
const { to, message, priority } = (args ?? {}) as unknown as SendMessageArgs;
if (!to || !message)
return text("send_message: `to` and `message` required", true);
@@ -477,9 +477,17 @@ Your message mode is "${messageMode}".
true,
);
const sections: string[] = [];
// Keep the status-line cache fresh for Claude Code's statusLine renderer.
const statusCache: Record<string, { total: number; online: number; updatedAt: string; you?: string }> = {};
for (const c of clients) {
const peers = await c!.listPeers();
const header = `## ${c!.meshSlug} (${c!.status}, mesh ${c!.meshId.slice(0, 8)}…)`;
statusCache[c!.meshSlug] = {
total: peers.length,
online: peers.filter(p => p.status !== "offline").length,
updatedAt: new Date().toISOString(),
you: process.env.CLAUDEMESH_DISPLAY_NAME ?? undefined,
};
if (peers.length === 0) {
sections.push(`${header}\nNo peers connected.`);
} else {
@@ -502,6 +510,15 @@ Your message mode is "${messageMode}".
sections.push(`${header}\n${peerLines.join("\n")}`);
}
}
// Persist the peer-cache for claudemesh status-line. Best effort.
try {
const { writeFileSync, mkdirSync, existsSync } = await import("node:fs");
const { join: joinPath } = await import("node:path");
const { homedir } = await import("node:os");
const dir = joinPath(homedir(), ".claudemesh");
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
writeFileSync(joinPath(dir, "peer-cache.json"), JSON.stringify(statusCache));
} catch { /* non-fatal */ }
return text(sections.join("\n\n"));
}
@@ -542,7 +559,7 @@ Your message mode is "${messageMode}".
}
case "set_summary": {
const { summary } = (args ?? {}) as SetSummaryArgs;
const { summary } = (args ?? {}) as unknown as SetSummaryArgs;
if (!summary) return text("set_summary: `summary` required", true);
for (const c of allClients()) await c.setSummary(summary);
return text(
@@ -551,7 +568,7 @@ Your message mode is "${messageMode}".
}
case "set_status": {
const { status } = (args ?? {}) as SetStatusArgs;
const { status } = (args ?? {}) as unknown as SetStatusArgs;
if (!status) return text("set_status: `status` required", true);
const s = status as PeerStatus;
for (const c of allClients()) await c.setStatus(s);
@@ -654,6 +671,8 @@ Your message mode is "${messageMode}".
cron?: string;
};
if (!sArgs.message) return text("schedule_reminder: `message` required", true);
const client = allClients()[0];
if (!client) return text("schedule_reminder: not connected", true);
const isCron = !!sArgs.cron;
@@ -710,6 +729,8 @@ Your message mode is "${messageMode}".
);
}
case "list_scheduled": {
const client = allClients()[0];
if (!client) return text("list_scheduled: not connected", true);
const scheduled = await client.listScheduled();
if (scheduled.length === 0) return text("No pending scheduled messages.");
const lines = scheduled.map((m) =>
@@ -718,6 +739,8 @@ Your message mode is "${messageMode}".
return text(`${scheduled.length} scheduled:\n${lines.join("\n")}`);
}
case "cancel_scheduled": {
const client = allClients()[0];
if (!client) return text("cancel_scheduled: not connected", true);
const { id: schedId } = (args ?? {}) as { id?: string };
if (!schedId) return text("cancel_scheduled: `id` required", true);
const ok = await client.cancelScheduled(schedId);
@@ -735,7 +758,7 @@ Your message mode is "${messageMode}".
// If 'to' specified, do E2E encryption
if (fileTo) {
const { encryptFile, sealKeyForPeer } = await import("../crypto/file-crypto");
const { encryptFile, sealKeyForPeer } = await import("~/services/crypto/file-crypto.js");
const { readFileSync, writeFileSync, mkdtempSync, unlinkSync, rmdirSync } = await import("node:fs");
const { tmpdir } = await import("node:os");
const { join, basename } = await import("node:path");
@@ -764,14 +787,15 @@ Your message mode is "${messageMode}".
];
// Build combined buffer: nonce (24 bytes) + ciphertext
const { ensureSodium } = await import("../crypto/keypair");
const { ensureSodium } = await import("~/services/crypto/keypair.js");
const sodium = await ensureSodium();
const nonceBytes = sodium.from_base64(nonce, sodium.base64_variants.ORIGINAL);
const combined = new Uint8Array(nonceBytes.length + ciphertext.length);
combined.set(nonceBytes, 0);
combined.set(ciphertext, nonceBytes.length);
const baseName = fileName ?? basename(filePath);
const rawName = fileName ?? basename(filePath);
const baseName = basename(rawName).replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 255);
const tmpDir = mkdtempSync(join(tmpdir(), "cm-"));
const tmpPath = join(tmpDir, baseName);
writeFileSync(tmpPath, combined);
@@ -814,32 +838,33 @@ Your message mode is "${messageMode}".
if (!result) return text(`get_file: file ${id} not found`, true);
if (result.encrypted) {
if (!result.sealedKey) return text("get_file: encrypted file — no decryption key available for your session", true);
const { openSealedKey, decryptFile } = await import("../crypto/file-crypto");
const { ensureSodium } = await import("../crypto/keypair");
const genericErr = "get_file: could not decrypt — you may not have access to this file";
if (!result.sealedKey) return text(genericErr, true);
const { openSealedKey, decryptFile } = await import("~/services/crypto/file-crypto.js");
const { ensureSodium } = await import("~/services/crypto/keypair.js");
const myPubkey = client.getSessionPubkey();
const mySecret = client.getSessionSecretKey();
if (!myPubkey || !mySecret) {
return text("get_file: no session keypair — cannot decrypt", true);
}
if (!myPubkey || !mySecret) return text(genericErr, true);
const kf = await openSealedKey(result.sealedKey, myPubkey, mySecret);
if (!kf) return text("get_file: failed to open sealed key", true);
if (!kf) return text(genericErr, true);
// Download file bytes from presigned URL
const MAX_DOWNLOAD = 100 * 1024 * 1024; // 100 MB
const resp = await fetch(result.url, { signal: AbortSignal.timeout(30_000) });
if (!resp.ok) return text(`get_file: download failed (${resp.status})`, true);
const contentLength = parseInt(resp.headers.get("content-length") ?? "0", 10);
if (contentLength > MAX_DOWNLOAD) return text(`get_file: file too large (${contentLength} bytes)`, true);
const buf = new Uint8Array(await resp.arrayBuffer());
if (buf.length > MAX_DOWNLOAD) return text(`get_file: file too large (${buf.length} bytes)`, true);
// Wire format: first 24 bytes = nonce, rest = ciphertext
const sodium = await ensureSodium();
const NONCE_BYTES = sodium.crypto_secretbox_NONCEBYTES; // 24
const NONCE_BYTES = sodium.crypto_secretbox_NONCEBYTES;
if (buf.length < NONCE_BYTES) return text(genericErr, true);
const nonce = sodium.to_base64(buf.slice(0, NONCE_BYTES), sodium.base64_variants.ORIGINAL);
const ciphertext = buf.slice(NONCE_BYTES);
const plaintext = await decryptFile(ciphertext, nonce, kf);
if (!plaintext) return text("get_file: decryption failed", true);
if (!plaintext) return text(genericErr, true);
const { writeFileSync, mkdirSync } = await import("node:fs");
const { dirname } = await import("node:path");
@@ -852,7 +877,7 @@ Your message mode is "${messageMode}".
let res = await fetch(result.url, { signal: AbortSignal.timeout(10_000) }).catch(() => null);
if (!res || !res.ok) {
// Presigned URL failed (internal MinIO hostname) — use broker proxy
const brokerHttp = client.mesh.brokerUrl.replace("wss://", "https://").replace("ws://", "http://").replace("/ws", "");
const brokerHttp = client.brokerUrl.replace("wss://", "https://").replace("ws://", "http://").replace("/ws", "");
res = await fetch(`${brokerHttp}/download/${id}?mesh=${client.meshId}`, { signal: AbortSignal.timeout(30_000) });
}
if (!res.ok) return text(`get_file: download failed (${res.status})`, true);
@@ -1379,7 +1404,7 @@ Your message mode is "${messageMode}".
if (!result.encrypted) return text("grant_file_access: file is not encrypted", true);
if (!result.sealedKey) return text("grant_file_access: no key available (are you the owner?)", true);
const { openSealedKey, sealKeyForPeer } = await import("../crypto/file-crypto");
const { openSealedKey, sealKeyForPeer } = await import("~/services/crypto/file-crypto.js");
const myPubkey = client.getSessionPubkey();
const mySecret = client.getSessionSecretKey();
if (!myPubkey || !mySecret) return text("grant_file_access: no session keypair", true);
@@ -1512,6 +1537,9 @@ Your message mode is "${messageMode}".
key?: string; value?: string; type?: "env" | "file"; mount_path?: string; description?: string;
};
if (!key || !value) return text("vault_set: `key` and `value` required", true);
if (!/^[a-zA-Z0-9_.-]{1,128}$/.test(key)) return text("vault_set: `key` must be 1-128 alphanumeric/underscore/dot/dash chars", true);
if (mount_path && (mount_path.includes("..") || mount_path.length > 512)) return text("vault_set: invalid `mount_path`", true);
if (description && description.length > 500) return text("vault_set: `description` too long (max 500 chars)", true);
const client = allClients()[0];
if (!client) return text("vault_set: not connected", true);
const entryType = vType ?? "env";
@@ -1527,12 +1555,12 @@ Your message mode is "${messageMode}".
}
// E2E encrypt: crypto_secretbox with random Kf, then seal Kf with mesh pubkey
const { encryptFile, sealKeyForPeer } = await import("../crypto/file-crypto");
const { encryptFile, sealKeyForPeer } = await import("~/services/crypto/file-crypto.js");
const { ciphertext, nonce, key: kf } = await encryptFile(plaintextBytes);
const sealedKey = await sealKeyForPeer(kf, client.getMeshPubkey());
// Convert ciphertext to base64 for storage
const { ensureSodium } = await import("../crypto/keypair");
const { ensureSodium } = await import("~/services/crypto/keypair.js");
const sodium = await ensureSodium();
const ciphertextB64 = sodium.to_base64(ciphertext, sodium.base64_variants.ORIGINAL);
@@ -1597,8 +1625,8 @@ Your message mode is "${messageMode}".
// Fetch + decrypt vault entries client-side
if (vaultRefs.length > 0) {
const { openSealedKey, decryptFile } = await import("../crypto/file-crypto");
const { ensureSodium } = await import("../crypto/keypair");
const { openSealedKey, decryptFile } = await import("~/services/crypto/file-crypto.js");
const { ensureSodium } = await import("~/services/crypto/keypair.js");
const sodium = await ensureSodium();
const keys = vaultRefs.map(r => r.vaultKey);
@@ -1606,15 +1634,15 @@ Your message mode is "${messageMode}".
for (const ref of vaultRefs) {
const entry = encryptedEntries.find((e: any) => e.key === ref.vaultKey);
if (!entry) return text(`mesh_mcp_deploy: vault key "${ref.vaultKey}" not found. Use vault_set first.`, true);
if (!entry) return text(`mesh_mcp_deploy: a referenced vault key was not found. Use vault_set first.`, true);
// Decrypt: open sealed key with mesh keypair, then decrypt ciphertext
const kf = await openSealedKey(entry.sealed_key, client.getMeshPubkey(), client.getMeshSecretKey());
if (!kf) return text(`mesh_mcp_deploy: failed to decrypt vault key "${ref.vaultKey}" — wrong keypair?`, true);
if (!kf) return text(`mesh_mcp_deploy: failed to decrypt a vault entry — wrong keypair?`, true);
const ciphertextBytes = sodium.from_base64(entry.ciphertext, sodium.base64_variants.ORIGINAL);
const plainBytes = await decryptFile(ciphertextBytes, entry.nonce, kf);
if (!plainBytes) return text(`mesh_mcp_deploy: failed to decrypt vault entry "${ref.vaultKey}" — corrupted?`, true);
if (!plainBytes) return text(`mesh_mcp_deploy: failed to decrypt a vault entry — data may be corrupted`, true);
if (ref.isFile && ref.mountPath) {
// For file-type entries: the plaintext is the file content (raw bytes).
@@ -1826,7 +1854,7 @@ Your message mode is "${messageMode}".
event: eventName,
mesh_slug: client.meshSlug,
mesh_id: client.meshId,
...(Object.keys(data).length > 0 ? { eventData: data } : {}),
...(Object.keys(data).length > 0 ? { eventData: JSON.stringify(data) } : {}),
},
},
});
@@ -1842,6 +1870,18 @@ Your message mode is "${messageMode}".
? await resolvePeerName(client, fromPubkey)
: "unknown";
// Per-peer capability check — drop silently if sender lacks `dm`.
if (fromPubkey) {
try {
const { isAllowed } = await import("~/commands/grants.js");
const kindCap = msg.kind === "broadcast" ? "broadcast" : "dm";
if (!isAllowed(client.meshSlug, fromPubkey, kindCap)) {
process.stderr.write(`[claudemesh] dropped ${kindCap} from ${fromName} (not granted)\n`);
return;
}
} catch { /* fail-open on grant-read errors — don't break delivery */ }
}
if (messageMode === "inbox") {
try {
await server.notification({
@@ -1855,8 +1895,13 @@ Your message mode is "${messageMode}".
return;
}
// push mode — full content
const content = msg.plaintext ?? decryptFailedWarning(fromPubkey);
// push mode — full content. Format the content so it reads as a
// first-class chat message even though Claude Code renders it as a
// <channel> reminder: sender attribution + priority badge + body.
const body = msg.plaintext ?? decryptFailedWarning(fromPubkey);
const prioBadge = msg.priority === "now" ? "[URGENT] " : msg.priority === "low" ? "[low] " : "";
const kindBadge = msg.kind === "broadcast" ? " (broadcast)" : "";
const content = `${prioBadge}${fromName}${kindBadge}: ${body}`;
try {
await server.notification({
method: "notifications/claude/channel",
@@ -1875,7 +1920,7 @@ Your message mode is "${messageMode}".
},
},
});
process.stderr.write(`[claudemesh] pushed: from=${fromName} content=${content.slice(0, 60)}\n`);
process.stderr.write(`[claudemesh] pushed: from=${fromName} content=${body.slice(0, 60)}\n`);
} catch (pushErr) {
process.stderr.write(`[claudemesh] push FAILED: ${pushErr}\n`);
}
@@ -1970,7 +2015,7 @@ Your message mode is "${messageMode}".
* Code will not auto-restart it.
*/
async function startServiceProxy(serviceName: string): Promise<void> {
const config = loadConfig();
const config = readConfig();
if (config.meshes.length === 0) {
process.stderr.write(`[mesh:${serviceName}] no meshes joined\n`);
process.exit(1);
@@ -2035,13 +2080,13 @@ async function startServiceProxy(serviceName: string): Promise<void> {
const args = req.params.arguments ?? {};
// Wait for broker reconnection if needed
if (client.status !== "open") {
if ((client.status as string) !== "open") {
let waited = 0;
while (client.status !== "open" && waited < 10_000) {
while ((client.status as string) !== "open" && waited < 10_000) {
await new Promise((r) => setTimeout(r, 500));
waited += 500;
}
if (client.status !== "open") {
if ((client.status as string) !== "open") {
return {
content: [
{
@@ -2105,7 +2150,7 @@ async function startServiceProxy(serviceName: string): Promise<void> {
// Refresh tools
const newTools = (push.eventData as any)?.tools;
if (Array.isArray(newTools)) {
tools = newTools;
tools = newTools as typeof tools;
// Notify Claude Code that tools changed
server
.notification({

View File

@@ -0,0 +1,4 @@
// 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

@@ -0,0 +1,4 @@
// 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

@@ -0,0 +1,4 @@
// 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

@@ -0,0 +1,4 @@
// 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

@@ -0,0 +1,4 @@
// 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

@@ -0,0 +1,21 @@
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

@@ -0,0 +1,4 @@
// 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

@@ -0,0 +1,4 @@
// 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

@@ -0,0 +1,4 @@
// 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

@@ -0,0 +1,4 @@
// 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

@@ -0,0 +1,4 @@
// 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

@@ -0,0 +1,4 @@
// 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

@@ -0,0 +1,4 @@
// 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

@@ -0,0 +1,4 @@
// 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

@@ -0,0 +1,4 @@
// 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

@@ -0,0 +1,4 @@
// 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

@@ -0,0 +1,4 @@
// 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

@@ -0,0 +1,4 @@
// 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

@@ -0,0 +1,4 @@
// 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

@@ -0,0 +1,4 @@
// 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

@@ -0,0 +1,4 @@
// 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

@@ -0,0 +1,4 @@
// 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;