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

99 lines
3.0 KiB
TypeScript

/**
* MCP tools for rSpace space discovery.
*
* Tools: list_spaces, list_modules
* Resource: rspace://spaces/{slug}
*/
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { listCommunities, loadCommunity, getDocumentData, normalizeVisibility } from "../community-store";
import { getModuleInfoList } from "../../shared/module";
import { resolveAccess } from "./_auth";
export function registerSpacesTools(server: McpServer) {
server.tool(
"list_spaces",
"List all rSpace spaces with metadata (name, visibility, owner, members, enabled modules)",
{
token: z.string().optional().describe("JWT auth token (shows private/permissioned spaces you have access to)"),
},
async ({ token }) => {
const slugs = await listCommunities();
const spaces = [];
for (const slug of slugs) {
await loadCommunity(slug);
const data = getDocumentData(slug);
if (!data) continue;
const visibility = normalizeVisibility(data.meta.visibility || "private");
// Filter out spaces the caller can't see
if (visibility !== "public") {
const access = await resolveAccess(token, slug, false);
if (!access.allowed) continue;
}
spaces.push({
slug,
name: data.meta.name,
visibility,
ownerDID: data.meta.ownerDID,
description: data.meta.description || null,
enabledModules: data.meta.enabledModules || null,
memberCount: Object.keys(data.members || {}).length,
shapeCount: Object.keys(data.shapes || {}).length,
createdAt: data.meta.createdAt,
});
}
return {
content: [{ type: "text", text: JSON.stringify(spaces, null, 2) }],
};
},
);
server.tool(
"list_modules",
"List all available rSpace modules (rApps) with metadata",
{},
async () => {
const modules = getModuleInfoList();
return {
content: [{ type: "text", text: JSON.stringify(modules, null, 2) }],
};
},
);
server.resource(
"space_metadata",
"rspace://spaces/{slug}",
{ description: "Get detailed metadata for a specific rSpace space" },
async (uri) => {
const slug = uri.pathname.split("/").pop();
if (!slug) {
return { contents: [{ uri: uri.href, mimeType: "application/json", text: '{"error":"Missing slug"}' }] };
}
await loadCommunity(slug);
const data = getDocumentData(slug);
if (!data) {
return { contents: [{ uri: uri.href, mimeType: "application/json", text: '{"error":"Space not found"}' }] };
}
const result = {
slug,
name: data.meta.name,
visibility: data.meta.visibility,
ownerDID: data.meta.ownerDID,
description: data.meta.description || null,
enabledModules: data.meta.enabledModules || null,
members: Object.values(data.members || {}),
shapeCount: Object.keys(data.shapes || {}).length,
nestedSpaceCount: Object.keys(data.nestedSpaces || {}).length,
createdAt: data.meta.createdAt,
};
return {
contents: [{ uri: uri.href, mimeType: "application/json", text: JSON.stringify(result, null, 2) }],
};
},
);
}