/** * MCP tools for rNetwork (CRM contacts & relationships). * * Tools: rnetwork_list_contacts, rnetwork_get_contact, rnetwork_list_relationships * Reads LOCAL Automerge doc only (not external Twenty CRM). */ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; import type { SyncServer } from "../local-first/sync-server"; import { networkDocId } from "../../modules/rnetwork/schemas"; import type { NetworkDoc } from "../../modules/rnetwork/schemas"; import { resolveAccess, accessDeniedResponse } from "./_auth"; export function registerNetworkTools(server: McpServer, syncServer: SyncServer) { server.tool( "rnetwork_list_contacts", "List CRM contacts in a space", { space: z.string().describe("Space slug"), token: z.string().optional().describe("JWT auth token (required for private/permissioned spaces)"), limit: z.number().optional().describe("Max results (default 50)"), }, async ({ space, token, limit }) => { const access = await resolveAccess(token, space, false); if (!access.allowed) return accessDeniedResponse(access.reason!); const doc = syncServer.getDoc(networkDocId(space)); if (!doc) { return { content: [{ type: "text", text: JSON.stringify({ error: "No network data found for this space" }) }] }; } const contacts = Object.values(doc.contacts || {}) .slice(0, limit || 50) .map(c => ({ did: c.did, name: c.name, role: c.role, tags: c.tags, addedAt: c.addedAt, })); return { content: [{ type: "text", text: JSON.stringify(contacts, null, 2) }] }; }, ); server.tool( "rnetwork_get_contact", "Get a specific contact and their relationships", { space: z.string().describe("Space slug"), token: z.string().optional().describe("JWT auth token"), did: z.string().describe("Contact DID"), }, async ({ space, token, did }) => { const access = await resolveAccess(token, space, false); if (!access.allowed) return accessDeniedResponse(access.reason!); const doc = syncServer.getDoc(networkDocId(space)); if (!doc) { return { content: [{ type: "text", text: JSON.stringify({ error: "No network data found" }) }] }; } const contact = doc.contacts?.[did]; if (!contact) { return { content: [{ type: "text", text: JSON.stringify({ error: "Contact not found" }) }] }; } const relationships = Object.values(doc.relationships || {}) .filter(r => r.fromDid === did || r.toDid === did); return { content: [{ type: "text", text: JSON.stringify({ contact, relationships }, null, 2) }] }; }, ); server.tool( "rnetwork_list_relationships", "List all relationships in a space's network", { space: z.string().describe("Space slug"), token: z.string().optional().describe("JWT auth token"), limit: z.number().optional().describe("Max results (default 100)"), }, async ({ space, token, limit }) => { const access = await resolveAccess(token, space, false); if (!access.allowed) return accessDeniedResponse(access.reason!); const doc = syncServer.getDoc(networkDocId(space)); if (!doc) { return { content: [{ type: "text", text: JSON.stringify({ error: "No network data found" }) }] }; } const relationships = Object.values(doc.relationships || {}) .slice(0, limit || 100); return { content: [{ type: "text", text: JSON.stringify(relationships, null, 2) }] }; }, ); }