/** * MCP tools for rSocials (campaigns & social threads). * * Tools: rsocials_list_campaigns, rsocials_get_campaign, * rsocials_list_threads, rsocials_create_thread */ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; import type { SyncServer } from "../local-first/sync-server"; import { socialsDocId } from "../../modules/rsocials/schemas"; import type { SocialsDoc } from "../../modules/rsocials/schemas"; import { resolveAccess, accessDeniedResponse } from "./_auth"; export function registerSocialsTools(server: McpServer, syncServer: SyncServer) { server.tool( "rsocials_list_campaigns", "List social media campaigns in a space", { space: z.string().describe("Space slug"), token: z.string().optional().describe("JWT auth token (required for private/permissioned spaces)"), }, async ({ space, token }) => { const access = await resolveAccess(token, space, false); if (!access.allowed) return accessDeniedResponse(access.reason!); const doc = syncServer.getDoc(socialsDocId(space)); if (!doc) { return { content: [{ type: "text", text: JSON.stringify({ error: "No socials data found for this space" }) }] }; } const campaigns = Object.values(doc.campaigns || {}).map(c => ({ id: c.id, title: c.title, description: c.description, platforms: c.platforms, postCount: c.posts?.length ?? 0, createdAt: c.createdAt, updatedAt: c.updatedAt, })); return { content: [{ type: "text", text: JSON.stringify(campaigns, null, 2) }] }; }, ); server.tool( "rsocials_get_campaign", "Get full details of a specific campaign including posts", { space: z.string().describe("Space slug"), token: z.string().optional().describe("JWT auth token"), campaign_id: z.string().describe("Campaign ID"), }, async ({ space, token, campaign_id }) => { const access = await resolveAccess(token, space, false); if (!access.allowed) return accessDeniedResponse(access.reason!); const doc = syncServer.getDoc(socialsDocId(space)); const campaign = doc?.campaigns?.[campaign_id]; if (!campaign) { return { content: [{ type: "text", text: JSON.stringify({ error: "Campaign not found" }) }] }; } return { content: [{ type: "text", text: JSON.stringify(campaign, null, 2) }] }; }, ); server.tool( "rsocials_list_threads", "List social threads in a space", { space: z.string().describe("Space slug"), token: z.string().optional().describe("JWT auth token"), 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(socialsDocId(space)); if (!doc) { return { content: [{ type: "text", text: JSON.stringify({ error: "No socials data found" }) }] }; } const threads = Object.values(doc.threads || {}) .sort((a, b) => b.updatedAt - a.updatedAt) .slice(0, limit || 50) .map(t => ({ id: t.id, name: t.name, handle: t.handle, title: t.title, tweetCount: t.tweets?.length ?? 0, createdAt: t.createdAt, updatedAt: t.updatedAt, })); return { content: [{ type: "text", text: JSON.stringify(threads, null, 2) }] }; }, ); server.tool( "rsocials_create_thread", "Create a new social thread (requires auth token + space membership)", { space: z.string().describe("Space slug"), token: z.string().describe("JWT auth token"), name: z.string().describe("Author display name"), handle: z.string().describe("Author handle"), title: z.string().describe("Thread title"), tweets: z.array(z.string()).describe("Tweet texts in order"), }, async ({ space, token, name, handle, title, tweets }) => { const access = await resolveAccess(token, space, true); if (!access.allowed) return accessDeniedResponse(access.reason!); const docId = socialsDocId(space); const doc = syncServer.getDoc(docId); if (!doc) { return { content: [{ type: "text", text: JSON.stringify({ error: "No socials data found" }) }], isError: true }; } const threadId = `thread-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; const now = Date.now(); syncServer.changeDoc(docId, `Create thread ${title}`, (d) => { if (!d.threads) (d as any).threads = {}; d.threads[threadId] = { id: threadId, name, handle, title, tweets, imageUrl: null, tweetImages: null, createdAt: now, updatedAt: now, }; }); return { content: [{ type: "text", text: JSON.stringify({ id: threadId, created: true }) }] }; }, ); }