/** * MCP tools for rSplat (3D gaussian splat scenes). * * Tools: rsplat_list_scenes, rsplat_get_scene * Omits filePath from responses. */ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; import type { SyncServer } from "../local-first/sync-server"; import { splatScenesDocId } from "../../modules/rsplat/schemas"; import type { SplatScenesDoc } from "../../modules/rsplat/schemas"; import { resolveAccess, accessDeniedResponse } from "./_auth"; export function registerSplatTools(server: McpServer, syncServer: SyncServer) { server.tool( "rsplat_list_scenes", "List 3D gaussian splat scenes in a space", { space: z.string().describe("Space slug"), token: z.string().optional().describe("JWT auth token"), search: z.string().optional().describe("Search in title/tags"), }, async ({ space, token, search }) => { const access = await resolveAccess(token, space, false); if (!access.allowed) return accessDeniedResponse(access.reason!); const doc = syncServer.getDoc(splatScenesDocId(space)); if (!doc) return { content: [{ type: "text", text: JSON.stringify({ error: "No splat data found" }) }] }; let items = Object.values(doc.items || {}); if (search) { const q = search.toLowerCase(); items = items.filter(i => i.title.toLowerCase().includes(q) || i.tags.some(t => t.toLowerCase().includes(q)), ); } const summary = items.map(i => ({ id: i.id, slug: i.slug, title: i.title, description: (i.description || "").slice(0, 200), fileFormat: i.fileFormat, fileSizeBytes: i.fileSizeBytes, tags: i.tags, status: i.status, processingStatus: i.processingStatus, viewCount: i.viewCount, sourceFileCount: i.sourceFileCount, thumbnailUrl: i.thumbnailUrl, createdAt: i.createdAt, })); return { content: [{ type: "text", text: JSON.stringify(summary, null, 2) }] }; }, ); server.tool( "rsplat_get_scene", "Get full scene metadata (omits filePath)", { space: z.string().describe("Space slug"), token: z.string().optional().describe("JWT auth token"), scene_id: z.string().describe("Scene ID"), }, async ({ space, token, scene_id }) => { const access = await resolveAccess(token, space, false); if (!access.allowed) return accessDeniedResponse(access.reason!); const doc = syncServer.getDoc(splatScenesDocId(space)); const scene = doc?.items?.[scene_id]; if (!scene) return { content: [{ type: "text", text: JSON.stringify({ error: "Scene not found" }) }] }; const { filePath, ...safe } = scene; return { content: [{ type: "text", text: JSON.stringify(safe, null, 2) }] }; }, ); }