Submatter/apps/mcp-server/src/tools/manage-object.ts

126 lines
4 KiB
TypeScript
Raw Normal View History

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);
}
},
);
}