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 { 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), workspaceId: z.string().uuid().optional(), 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) with optional filters.", inputSchema: searchObjectsInputSchema, }, async (args) => { try { const input = searchObjectsInputSchema.parse(args); const limit = input.limit ?? 50; const pattern = `%${escapeLikePattern(input.query)}%`; const conditions = [ isNull(objects.archivedAt), or(ilike(objects.title, pattern), ilike(objects.description, pattern)), ]; if (input.workspaceId) { conditions.push(eq(objects.workspaceId, input.workspaceId)); } 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({ objects: rows, count: rows.length }); } catch (e) { return toolCatch(e); } }, ); }