rspace-online/server/mcp-tools/rgov.ts

82 lines
2.8 KiB
TypeScript

/**
* MCP tools for rGov (modular governance decision circuits).
* rGov shapes live in the space's main canvas document, not a separate Automerge doc.
*
* Tools: rgov_list_circuits, rgov_get_circuit
*/
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { getDocumentData } from "../community-store";
import { resolveAccess, accessDeniedResponse } from "./_auth";
const GOV_TYPES = [
"folk-gov-binary", "folk-gov-threshold", "folk-gov-knob",
"folk-gov-project", "folk-gov-amendment",
"folk-gov-quadratic", "folk-gov-conviction", "folk-gov-multisig",
"folk-gov-sankey",
];
export function registerGovTools(server: McpServer) {
server.tool(
"rgov_list_circuits",
"List governance circuit shapes on the space canvas",
{
space: z.string().describe("Space slug"),
token: z.string().optional().describe("JWT auth token"),
type: z.string().optional().describe("Filter by gov shape type (e.g. folk-gov-project, folk-gov-threshold)"),
},
async ({ space, token, type }) => {
const access = await resolveAccess(token, space, false);
if (!access.allowed) return accessDeniedResponse(access.reason!);
const docData = getDocumentData(space);
if (!docData?.shapes) return { content: [{ type: "text" as const, text: JSON.stringify({ shapes: [] }) }] };
let govShapes = Object.values(docData.shapes)
.filter((s: any) => !s.forgotten && GOV_TYPES.includes(s.type));
if (type) govShapes = govShapes.filter((s: any) => s.type === type);
const summary = govShapes.map((s: any) => ({
id: s.id, type: s.type, title: s.title,
status: s.status, x: s.x, y: s.y,
}));
return { content: [{ type: "text" as const, text: JSON.stringify(summary, null, 2) }] };
},
);
server.tool(
"rgov_get_circuit",
"Get full details of a governance shape by ID",
{
space: z.string().describe("Space slug"),
token: z.string().optional().describe("JWT auth token"),
shape_id: z.string().describe("Shape ID"),
},
async ({ space, token, shape_id }) => {
const access = await resolveAccess(token, space, false);
if (!access.allowed) return accessDeniedResponse(access.reason!);
const docData = getDocumentData(space);
const shape = docData?.shapes?.[shape_id] as any;
if (!shape || !GOV_TYPES.includes(shape.type)) {
return { content: [{ type: "text" as const, text: JSON.stringify({ error: "Gov shape not found" }) }] };
}
// Find arrows connected to this shape
const arrows = Object.values(docData!.shapes)
.filter((s: any) => s.type === "folk-arrow" && (s.sourceId === shape_id || s.targetId === shape_id))
.map((a: any) => ({ id: a.id, sourceId: a.sourceId, targetId: a.targetId }));
return {
content: [{
type: "text" as const,
text: JSON.stringify({ shape, connectedArrows: arrows }, null, 2),
}],
};
},
);
}