import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { and, eq } from "../drizzle.js"; import { z } from "zod"; import { db } from "../db.js"; import { objectAssignees, objects, views } from "../schema.js"; import { viewTypes } from "../shared-types.js"; import { toolCatch, toolErr, toolOk } from "./tool-result.js"; const viewTypeSchema = z.enum(viewTypes as unknown as [string, ...string[]]); const manageObjectInputSchema = z.discriminatedUnion("operation", [ z.object({ operation: z.literal("move_object"), id: z.string().uuid(), newParentId: z.string().uuid().nullable(), }), z.object({ operation: z.literal("assign_object"), objectId: z.string().uuid(), userId: z.string().uuid(), role: z.string().optional(), action: z.enum(["add", "remove"]), }), z.object({ operation: z.literal("create_view"), objectId: z.string().uuid(), viewType: viewTypeSchema, name: z.string().min(1).max(255), config: z.record(z.unknown()).optional(), }), z.object({ operation: z.literal("apply_template"), objectId: z.string().uuid(), templateId: z.string().uuid(), }), ]); export function registerManageObjectTool(mcp: McpServer): void { mcp.registerTool( "manage_object", { description: "Manage objects: move_object (change parent), assign_object (add/remove assignee), create_view, or apply_template (set template on object).", inputSchema: manageObjectInputSchema, }, async (args) => { try { const input = manageObjectInputSchema.parse(args); if (input.operation === "move_object") { const [updated] = await db .update(objects) .set({ parentId: input.newParentId, updatedAt: new Date() }) .where(eq(objects.id, input.id)) .returning(); if (!updated) { return toolErr(`Object not found: ${input.id}`); } return toolOk({ operation: input.operation, object: updated }); } if (input.operation === "assign_object") { if (input.action === "add") { const role = input.role ?? "assignee"; await db .insert(objectAssignees) .values({ objectId: input.objectId, userId: input.userId, role, }) .onConflictDoUpdate({ target: [objectAssignees.objectId, objectAssignees.userId], set: { role }, }); return toolOk({ operation: input.operation, action: input.action, ok: true }); } const deleted = await db .delete(objectAssignees) .where( and( eq(objectAssignees.objectId, input.objectId), eq(objectAssignees.userId, input.userId), ), ) .returning(); return toolOk({ operation: input.operation, action: input.action, removed: deleted[0] ?? null, }); } if (input.operation === "create_view") { const [created] = await db .insert(views) .values({ objectId: input.objectId, viewType: input.viewType, name: input.name, config: input.config ?? null, }) .returning(); if (!created) { return toolErr("Failed to create view"); } return toolOk({ operation: input.operation, view: created }); } const [updated] = await db .update(objects) .set({ templateId: input.templateId, updatedAt: new Date() }) .where(eq(objects.id, input.objectId)) .returning(); if (!updated) { return toolErr(`Object not found: ${input.objectId}`); } return toolOk({ operation: input.operation, object: updated }); } catch (e) { return toolCatch(e); } }, ); }