import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { and, asc, eq, ilike, isNull, or } from "../drizzle.js"; import { z } from "zod"; import { db } from "../db.js"; import { objects } from "../schema.js"; import { resolveWorkspaceHandle } from "../lib/resolve-workspace.js"; import { toolCatch, toolOk } from "./tool-result.js"; function escapeLikePattern(q: string): string { return q.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_"); } const searchObjectsInputSchema = z.object({ query: z.string().min(1), workspace: z .string() .min(1) .describe("Workspace UUID or slug. Required so search is tenant-scoped."), type: z.string().optional(), status: z.string().optional(), limit: z.number().int().positive().max(500).optional(), }); export function registerSearchObjectsTool(mcp: McpServer): void { mcp.registerTool( "search_objects", { description: "Search objects by text query (case-insensitive match on title and description) within a workspace. Accepts the workspace slug or UUID.", inputSchema: searchObjectsInputSchema, }, async (args) => { try { const input = searchObjectsInputSchema.parse(args); const ws = await resolveWorkspaceHandle(input.workspace); const limit = input.limit ?? 50; const pattern = `%${escapeLikePattern(input.query)}%`; const conditions = [ eq(objects.workspaceId, ws.id), isNull(objects.archivedAt), or(ilike(objects.title, pattern), ilike(objects.description, pattern)), ]; if (input.type !== undefined) { conditions.push(eq(objects.type, input.type)); } if (input.status !== undefined) { conditions.push(eq(objects.status, input.status)); } const rows = await db .select() .from(objects) .where(and(...conditions)) .orderBy(asc(objects.sortOrder), asc(objects.id)) .limit(limit); return toolOk({ workspace: { id: ws.id, slug: ws.slug, name: ws.name }, objects: rows, count: rows.length, }); } catch (e) { return toolCatch(e); } }, ); }