/** * MCP tools for rCred (contribution recognition via CredRank). * * Tools: rcred_get_scores, rcred_get_contributor, rcred_trigger_recompute */ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; import type { SyncServer } from "../local-first/sync-server"; import { scoresDocId, configDocId } from "../../modules/rcred/schemas"; import type { CredScoresDoc, CredConfigDoc } from "../../modules/rcred/schemas"; import { recomputeSpace, ensureConfigDoc } from "../../modules/rcred/grain-engine"; import { resolveAccess, accessDeniedResponse } from "./_auth"; export function registerCredTools(server: McpServer, syncServer: SyncServer) { server.tool( "rcred_get_scores", "Get CredRank contribution leaderboard for a space (sorted by cred descending)", { 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(scoresDocId(space)); if (!doc || !doc.scores) { return { content: [{ type: "text" as const, text: JSON.stringify({ scores: [], totalCred: 0 }) }] }; } const scores = Object.values(doc.scores) .sort((a, b) => b.cred - a.cred) .slice(0, limit || 50) .map((s, i) => ({ rank: i + 1, did: s.did, label: s.label, cred: s.cred, grainLifetime: s.grainLifetime, topModule: Object.entries(s.breakdown).sort(([, a], [, b]) => b - a)[0]?.[0] || '', })); return { content: [{ type: "text" as const, text: JSON.stringify({ scores, totalCred: doc.totalCred, computedAt: doc.computedAt, epochId: doc.epochId, }, null, 2), }], }; }, ); server.tool( "rcred_get_contributor", "Get detailed CredRank scores and module breakdown for a specific contributor", { space: z.string().describe("Space slug"), did: z.string().describe("Contributor DID"), token: z.string().optional().describe("JWT auth token"), }, async ({ space, did, token }) => { const access = await resolveAccess(token, space, false); if (!access.allowed) return accessDeniedResponse(access.reason!); const doc = syncServer.getDoc(scoresDocId(space)); if (!doc?.scores) { return { content: [{ type: "text" as const, text: JSON.stringify({ error: "No scores data found" }) }] }; } const score = doc.scores[did]; if (!score) { return { content: [{ type: "text" as const, text: JSON.stringify({ error: "Contributor not found" }) }] }; } return { content: [{ type: "text" as const, text: JSON.stringify(score, null, 2) }] }; }, ); server.tool( "rcred_trigger_recompute", "Trigger an immediate CredRank recompute for a space (requires auth token with member+ role)", { space: z.string().describe("Space slug"), token: z.string().describe("JWT auth token (member+ required)"), }, async ({ space, token }) => { const access = await resolveAccess(token, space, true); if (!access.allowed) return accessDeniedResponse(access.reason!); const result = recomputeSpace(space, syncServer); return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] }; }, ); }