diff --git a/integrations/enhanced-mcp/index.ts b/integrations/enhanced-mcp/index.ts index 32e5430b6..779a906ad 100644 --- a/integrations/enhanced-mcp/index.ts +++ b/integrations/enhanced-mcp/index.ts @@ -149,12 +149,16 @@ server.registerTool( } if (mode === "text") { + // `p_filter` is a metadata *containment* test in search_thoughts_text + // (`t.metadata @> coalesce(p_filter, '{}')`), not a bag of control + // flags. Adding exclude_restricted/start_date/end_date here asks for + // thoughts whose metadata literally contains those keys, which no + // thought does — so every text query matched zero rows. Only genuine + // metadata filters may go into the payload; sensitivity and date + // bounds are applied below, as the semantic branch already does. const filter: Record = { ...(metadataFilter as Record), }; - filter.exclude_restricted = true; - if (startDate) filter.start_date = startDate; - if (endDate) filter.end_date = endDate; const { data, error } = await supabase.rpc("search_thoughts_text", { p_query: query, @@ -167,7 +171,10 @@ server.registerTool( throw new Error(`search_thoughts_text failed: ${error.message}`); } - const rows = (data ?? []) as ThoughtRow[]; + const rows = ((data ?? []) as ThoughtRow[]) + .filter((row) => row.sensitivity_tier !== "restricted") + .filter((row) => !startDate || row.created_at >= startDate) + .filter((row) => !endDate || row.created_at <= endDate); const totalCount = rows.length > 0 ? Number( @@ -212,15 +219,14 @@ server.registerTool( // over-fetch slack to avoid silently returning zero results on active // brains with old date windows. See `known limitations` in the README. const dateFilterActive = !!(startDate || endDate); - // Forward filters into the RPC payload — ignored by older RPC versions - // but used by versions that support them, at which point the - // post-filter becomes a no-op. + // Only genuine metadata filters belong in the RPC payload. The getting- + // started `match_thoughts` applies `filter = '{}' OR metadata @> filter`, + // so unknown keys are NOT ignored as previously assumed here — they make + // the containment test fail and the query returns zero rows. Sensitivity + // and date bounds are post-filtered below instead. const semanticFilter: Record = { ...(metadataFilter as Record), - exclude_restricted: true, }; - if (startDate) semanticFilter.start_date = startDate; - if (endDate) semanticFilter.end_date = endDate; // Over-fetch when date filter is active so client-side post-filter // has headroom. 3x the requested limit is a reasonable compromise @@ -387,17 +393,16 @@ server.registerTool( description: "Fetch a thought by ID with its full metadata and provenance.", inputSchema: z.object({ - id: z.number().int().min(1).describe("Thought ID"), + // The core Open Brain schema defines `thoughts.id` as + // `uuid default gen_random_uuid()`, so an integer declaration made this + // tool impossible to call on a standard install. The lookup below is a + // plain `.eq()`, so a uuid string works unchanged. + id: z.string().uuid().describe("Thought ID (uuid)"), }), }, async (params) => { try { - const id = asInteger( - (params as Record).id, - 0, - 1, - Number.MAX_SAFE_INTEGER, - ); + const id = String((params as Record).id ?? "").trim(); if (!id) { return toolFailure("id is required"); @@ -687,17 +692,39 @@ server.registerTool( throw new Error(`upsert_thought failed: ${error.message}`); } - const result = data as UpsertThoughtResult | null; - if (!result?.thought_id) { + // The `upsert_thought` shipped in schemas/enhanced-thoughts returns + // `{id, fingerprint}` and never reads `p_payload.embedding` — the + // embedding column is not in its INSERT list. Reading only `thought_id` + // therefore threw *after* the row had already been inserted, leaving a + // thought with a NULL embedding that semantic search can never return. + // Accept either key, then persist the embedding in a follow-up update, + // which is the same two-step the stock core server performs. + const result = data as (UpsertThoughtResult & { id?: string }) | null; + const thoughtId = result?.thought_id ?? result?.id; + if (!thoughtId) { throw new Error("upsert_thought returned no result"); } + if (safeEmbedding(prepared.embedding)) { + const { error: embError } = await supabase + .from("thoughts") + .update({ embedding: prepared.embedding }) + .eq("id", thoughtId); + if (embError) { + return toolFailure( + `Thought ${thoughtId} saved, but its embedding failed to store (${embError.message}). ` + + `It will not appear in semantic search until re-embedded.`, + ); + } + } + return toolSuccess( - `${result.action === "inserted" ? "Captured new" : "Updated"} thought #${result.thought_id} as ${prepared.type}.`, + `${result?.action === "inserted" ? "Captured new" : "Saved"} thought #${thoughtId} as ${prepared.type}.`, { - thought_id: result.thought_id, - action: result.action, - content_fingerprint: result.content_fingerprint, + thought_id: thoughtId, + action: result?.action, + content_fingerprint: result?.content_fingerprint ?? + (result as Record | null)?.fingerprint, type: prepared.type, sensitivity_tier: prepared.sensitivity_tier, metadata: prepared.metadata, @@ -804,7 +831,10 @@ server.registerTool( const { data, error } = await supabase.rpc("search_thoughts_text", { p_query: query, p_limit: limit, - p_filter: { exclude_restricted: true }, + // Was `{ exclude_restricted: true }`, which the RPC treats as a + // metadata containment test and so matched nothing. Restricted rows + // are dropped client-side below instead. + p_filter: {}, p_offset: offset, }); @@ -812,7 +842,8 @@ server.registerTool( throw new Error(`search_thoughts_text failed: ${error.message}`); } - const rows = (data ?? []) as ThoughtRow[]; + const rows = ((data ?? []) as ThoughtRow[]) + .filter((row) => row.sensitivity_tier !== "restricted"); if (rows.length === 0) { return toolSuccess("No matches found.", { results: [] });