From 504111c50c5b90d33432bf76496da19183d3673f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Guti=C3=A9rrez?= <35082514+alezmad@users.noreply.github.com> Date: Tue, 7 Apr 2026 23:56:42 +0100 Subject: [PATCH] feat: add read_peer_file and list_peer_files MCP tools Wire up MCP tool handlers for the peer file sharing relay. Peers can now read files and list directories from other peers' local filesystems through the mesh broker. Includes name-to-pubkey resolution, base64 decode, and instructions table update. Co-Authored-By: Claude Sonnet 4.6 --- apps/cli/src/mcp/server.ts | 105 +++++++++++++++++++++++++++++++++++++ apps/cli/src/mcp/tools.ts | 33 ++++++++++++ 2 files changed, 138 insertions(+) diff --git a/apps/cli/src/mcp/server.ts b/apps/cli/src/mcp/server.ts index 7c88359..04d33c9 100644 --- a/apps/cli/src/mcp/server.ts +++ b/apps/cli/src/mcp/server.ts @@ -196,6 +196,8 @@ If the channel meta contains \`subtype: reminder\`, this is a scheduled reminder | schedule_reminder(message, in_seconds?, deliver_at?, to?) | Schedule a reminder to yourself (no \`to\`) or a delayed message to a peer/group. Delivered as a push with \`subtype: reminder\` in the channel meta. | | list_scheduled() | List pending scheduled reminders and messages. | | cancel_scheduled(id) | Cancel a pending scheduled item. | +| read_peer_file(peer, path) | Read a file from another peer's project (max 1MB). | +| list_peer_files(peer, path?, pattern?) | List files in a peer's shared directory. | | mesh_mcp_register(server_name, description, tools) | Register an MCP server with the mesh. Other peers can call its tools. | | mesh_mcp_list() | List MCP servers available in the mesh with their tools. | | mesh_tool_call(server_name, tool_name, args?) | Call a tool on a mesh-registered MCP server (30s timeout). | @@ -1196,6 +1198,109 @@ Your message mode is "${messageMode}". return text(`Access granted: ${targetPeer.displayName} can now download file ${fileId}`); } + // --- Peer file sharing --- + case "read_peer_file": { + const { peer: peerName, path: filePath } = (args ?? {}) as { peer?: string; path?: string }; + if (!peerName || !filePath) return text("read_peer_file: `peer` and `path` required", true); + const client = allClients()[0]; + if (!client) return text("read_peer_file: not connected", true); + + // Resolve peer name to pubkey + const peers = await client.listPeers(); + const nameLower = peerName.toLowerCase(); + let targetPubkey: string | null = null; + // Direct pubkey? + if (/^[0-9a-f]{64}$/.test(peerName)) { + targetPubkey = peerName; + } else { + const match = peers.find(p => p.displayName.toLowerCase() === nameLower); + if (!match) { + const partials = peers.filter(p => p.displayName.toLowerCase().includes(nameLower)); + if (partials.length === 1) { + targetPubkey = partials[0]!.pubkey; + } else { + const names = peers.map(p => p.displayName).join(", "); + return text(`read_peer_file: peer "${peerName}" not found. Online: ${names || "(none)"}`, true); + } + } else { + targetPubkey = match.pubkey; + } + } + + const result = await client.requestFile(targetPubkey, filePath); + if (result.error) return text(`read_peer_file: ${result.error}`, true); + if (!result.content) return text("read_peer_file: empty response from peer", true); + + // Decode base64 + try { + const decoded = Buffer.from(result.content, "base64").toString("utf-8"); + return text(decoded); + } catch { + return text("read_peer_file: failed to decode file content (binary file?)", true); + } + } + + case "list_peer_files": { + const { peer: peerName, path: dirPath, pattern } = (args ?? {}) as { peer?: string; path?: string; pattern?: string }; + if (!peerName) return text("list_peer_files: `peer` required", true); + const client = allClients()[0]; + if (!client) return text("list_peer_files: not connected", true); + + // Resolve peer name to pubkey + const peers = await client.listPeers(); + const nameLower = peerName.toLowerCase(); + let targetPubkey: string | null = null; + if (/^[0-9a-f]{64}$/.test(peerName)) { + targetPubkey = peerName; + } else { + const match = peers.find(p => p.displayName.toLowerCase() === nameLower); + if (!match) { + const partials = peers.filter(p => p.displayName.toLowerCase().includes(nameLower)); + if (partials.length === 1) { + targetPubkey = partials[0]!.pubkey; + } else { + const names = peers.map(p => p.displayName).join(", "); + return text(`list_peer_files: peer "${peerName}" not found. Online: ${names || "(none)"}`, true); + } + } else { + targetPubkey = match.pubkey; + } + } + + const result = await client.requestDir(targetPubkey, dirPath ?? ".", pattern); + if (result.error) return text(`list_peer_files: ${result.error}`, true); + if (!result.entries || result.entries.length === 0) return text("No files found."); + + return text(result.entries.join("\n")); + } + + // --- Webhooks --- + case "create_webhook": { + const { name: whName } = (args ?? {}) as { name?: string }; + if (!whName) return text("create_webhook: `name` required", true); + const client = allClients()[0]; + if (!client) return text("create_webhook: not connected", true); + const wh = await client.createWebhook(whName); + if (!wh) return text("create_webhook: broker did not acknowledge — check connection", true); + return text(`Webhook **${wh.name}** created.\n\nURL: ${wh.url}\nSecret: ${wh.secret}\n\nExternal services can POST JSON to this URL. The payload will be pushed to all connected mesh peers.`); + } + case "list_webhooks": { + const client = allClients()[0]; + if (!client) return text("list_webhooks: not connected", true); + const webhooks = await client.listWebhooks(); + if (webhooks.length === 0) return text("No active webhooks."); + const lines = webhooks.map(w => `- **${w.name}** — ${w.url} (created ${w.createdAt})`); + return text(`${webhooks.length} webhook(s):\n${lines.join("\n")}`); + } + case "delete_webhook": { + const { name: delName } = (args ?? {}) as { name?: string }; + if (!delName) return text("delete_webhook: `name` required", true); + const client = allClients()[0]; + if (!client) return text("delete_webhook: not connected", true); + const ok = await client.deleteWebhook(delName); + return text(ok ? `Webhook "${delName}" deactivated.` : `Failed to deactivate webhook "${delName}".`, !ok); + } + default: return text(`Unknown tool: ${name}`, true); } diff --git a/apps/cli/src/mcp/tools.ts b/apps/cli/src/mcp/tools.ts index 81f01e3..0fb14db 100644 --- a/apps/cli/src/mcp/tools.ts +++ b/apps/cli/src/mcp/tools.ts @@ -856,4 +856,37 @@ export const TOOLS: Tool[] = [ required: ["peer"], }, }, + + // --- Webhooks --- + { + name: "create_webhook", + description: + "Create an inbound webhook. Returns a URL that external services (GitHub, CI/CD, monitoring) can POST to — the payload becomes a mesh message to all peers.", + inputSchema: { + type: "object", + properties: { + name: { + type: "string", + description: "Webhook name (e.g. 'github-ci', 'datadog-alerts')", + }, + }, + required: ["name"], + }, + }, + { + name: "list_webhooks", + description: "List active webhooks for this mesh.", + inputSchema: { type: "object", properties: {} }, + }, + { + name: "delete_webhook", + description: "Deactivate a webhook.", + inputSchema: { + type: "object", + properties: { + name: { type: "string", description: "Webhook name to deactivate" }, + }, + required: ["name"], + }, + }, ];