diff --git a/integrations/delete-thought-mcp/README.md b/integrations/delete-thought-mcp/README.md new file mode 100644 index 00000000..de1474ba --- /dev/null +++ b/integrations/delete-thought-mcp/README.md @@ -0,0 +1,135 @@ +# Delete Thought MCP + +> Standalone MCP Edge Function that adds a `delete_thought` tool — hard-deletes a thought by UUID with a pre-flight fetch and a clear confirmation response. + +## What It Does + +The core Open Brain MCP server exposes capture/search/list/stats tools but has no delete path. As a result, thoughts accumulate forever — there is no way for an AI client to remove a test entry, a duplicate, or something captured in error without dropping into the Supabase SQL editor. + +This integration deploys a second Edge Function that exposes exactly one tool, `delete_thought(id)`. It is a hard delete (the row is removed), with a pre-flight existence check so the caller sees a distinct "not found" outcome rather than a silent success. + +**Recovery:** this is a hard delete, not a soft delete. Recovery depends on your Supabase project's database backups (daily backups are available on paid tiers; Point-in-Time Recovery on higher tiers). If you need recoverable deletes, install the companion `schemas/thought-audit` schema and extend this function to write an audit row with the prior content before the delete — see the "Audit hook" section below. + +## Prerequisites + +- Working Open Brain setup ([guide](../../docs/01-getting-started.md)) +- Supabase CLI installed + +## Credential Tracker + +Copy this block into a text editor and fill it in as you go. + +```text +DELETE THOUGHT MCP -- CREDENTIAL TRACKER +-------------------------------------- + +FROM YOUR OPEN BRAIN SETUP + Project URL: ____________ + Service role key: ____________ + MCP access key: ____________ + +GENERATED DURING SETUP + Delete Thought URL: https://.supabase.co/functions/v1/delete-thought-mcp + Custom connector name: Open Brain — Delete + +-------------------------------------- +``` + +## Steps + +### 1. Create the Edge Function + +From the root of your local Open Brain repo: + +**1. Create the function folder:** + +```bash +supabase functions new delete-thought-mcp +``` + +**2. Copy the integration code:** + +```bash +curl -o supabase/functions/delete-thought-mcp/index.ts \ + https://raw.githubusercontent.com/NateBJones-Projects/OB1/main/integrations/delete-thought-mcp/index.ts +curl -o supabase/functions/delete-thought-mcp/deno.json \ + https://raw.githubusercontent.com/NateBJones-Projects/OB1/main/integrations/delete-thought-mcp/deno.json +``` + +### 2. Set environment variables + +```bash +supabase secrets set MCP_ACCESS_KEY="your-mcp-access-key" +``` + +`SUPABASE_URL` and `SUPABASE_SERVICE_ROLE_KEY` are injected automatically by the Supabase platform. + +### 3. Deploy + +```bash +supabase functions deploy delete-thought-mcp --no-verify-jwt +``` + +### 4. Register the connector + +In Claude Desktop: **Settings → Connectors → Add custom connector**, paste: + +``` +https://.supabase.co/functions/v1/delete-thought-mcp?key= +``` + +Use a distinct connector name (e.g. `Open Brain — Delete`) so the tool is easy to spot in your tool list. + +### 5. Verify + +Ask Claude: `Call the delete_thought tool with id = "".` + +Run through this short verification sequence: + +1. Capture a throwaway thought and copy its id from the response. +2. Call `delete_thought` with that id — you should see `Deleted thought (prior content length: N chars).` +3. Call `delete_thought` with the same id again — you should see `Thought not found: ` with `isError: true`. +4. Confirm in the Supabase Table Editor that the row is gone (reload the Table Editor if it still appears cached). + +## Expected Outcome + +- A new Edge Function at `https://.supabase.co/functions/v1/delete-thought-mcp`. +- A custom connector in your AI client that exposes exactly one tool, `delete_thought`. +- Invoking the tool with a valid UUID removes that row from the `thoughts` table and returns a confirmation. +- Invoking with a non-existent UUID returns a clear `Thought not found: ` error. + +The [MCP Tool Audit & Optimization Guide](../../docs/05-tool-audit.md) explains how to manage your tool surface area as you add this and other custom connectors. + +## Audit Hook (optional) + +If you also install `schemas/thought-audit`, extend this function to write an audit row before the delete so the prior `content`, `metadata`, and `created_at` are preserved in `thought_audit` for recovery or historical audit queries. A minimal sketch: + +```ts +// Before the delete call: +await supabase.from("thought_audit").insert({ + thought_id: id, + action: "delete", + diff: { + previous_content: existing.content, + previous_metadata: existing.metadata ?? null, + }, + actor_context: { origin: "mcp:delete_thought" }, +}); +``` + +Left out of the base integration to keep its dependencies to a single table. + +## Troubleshooting + +**Issue: Tool call returns `401 Invalid or missing access key`** +Solution: Confirm the `?key=` in your custom connector URL matches the `MCP_ACCESS_KEY` secret set on the Edge Function. If you rotate the key, re-deploy and update the connector URL. + +**Issue: `delete_thought error: permission denied for table thoughts`** +Solution: Ensure your service role has DELETE permission on `public.thoughts`. The getting-started guide grants this in Step 2.5 — re-run `grant select, insert, update, delete on table public.thoughts to service_role;` in the SQL editor if it was missed. + +**Issue: Tool succeeds but the row is still visible in the Table Editor** +Solution: The Table Editor caches results. Reload the page, or run `select id from thoughts where id = ''` directly in the SQL Editor to confirm the row is gone. + +## Attribution + +Adapted from a multi-participant capture design used across live Claude / ChatGPT / Codex sessions. Released as a standalone integration so any Open Brain user can opt in without modifying the core server. diff --git a/integrations/delete-thought-mcp/deno.json b/integrations/delete-thought-mcp/deno.json new file mode 100644 index 00000000..5f87fd0c --- /dev/null +++ b/integrations/delete-thought-mcp/deno.json @@ -0,0 +1,5 @@ +{ + "imports": { + "@supabase/supabase-js": "npm:@supabase/supabase-js@2.47.10" + } +} diff --git a/integrations/delete-thought-mcp/index.ts b/integrations/delete-thought-mcp/index.ts new file mode 100644 index 00000000..dfa401c8 --- /dev/null +++ b/integrations/delete-thought-mcp/index.ts @@ -0,0 +1,155 @@ +/** + * delete-thought-mcp — Standalone MCP Edge Function that adds a single tool: + * delete_thought(id) + * + * The core open-brain MCP server does not expose a delete path. This + * integration adds one without modifying the core server — deploy alongside + * your main MCP connector and register as a separate custom connector. + * + * Behavior: + * - Pre-flight fetch to confirm the thought exists (so the caller gets a + * clear "not found" instead of a silent success). + * - Hard delete — the row is gone once this returns. Recovery depends on + * your database backup strategy (see README). + * + * Auth: x-brain-key header OR ?key=... URL query parameter. + * + * Env vars: + * SUPABASE_URL + * SUPABASE_SERVICE_ROLE_KEY + * MCP_ACCESS_KEY + * + * Extension hook: + * If you install the thought_audit schema (see `schemas/thought-audit`) + * you can extend this function to write an audit row before the delete + * so the prior content is preserved for recovery. Left out of the base + * integration to keep dependencies minimal. + */ + +import "jsr:@supabase/functions-js/edge-runtime.d.ts"; + +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { StreamableHTTPTransport } from "@hono/mcp"; +import { Hono } from "hono"; +import { z } from "zod"; +import { createClient } from "@supabase/supabase-js"; + +const SUPABASE_URL = Deno.env.get("SUPABASE_URL")!; +const SUPABASE_SERVICE_ROLE_KEY = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!; +const MCP_ACCESS_KEY = Deno.env.get("MCP_ACCESS_KEY")!; + +const supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY); + +// --- MCP Server Setup --- + +const server = new McpServer({ + name: "open-brain-delete-thought", + version: "1.0.0", +}); + +server.registerTool( + "delete_thought", + { + title: "Delete Thought", + description: + "Permanently delete a thought by UUID. The row is hard-deleted — recovery depends on your database backups. Returns a confirmation including the prior content length so the caller can log what was removed.", + inputSchema: { + id: z.string().uuid().describe("UUID of the thought to delete"), + }, + }, + async ({ id }) => { + try { + // Pre-flight fetch so "not found" is a clear, distinct outcome. + const { data: existing, error: fetchError } = await supabase + .from("thoughts") + .select("id, content") + .eq("id", id) + .single(); + + if (fetchError || !existing) { + return { + content: [ + { + type: "text" as const, + text: `Thought not found: ${id}`, + }, + ], + isError: true, + }; + } + + const { error } = await supabase.from("thoughts").delete().eq("id", id); + + if (error) { + return { + content: [ + { + type: "text" as const, + text: `delete_thought error: ${error.message}`, + }, + ], + isError: true, + }; + } + + const priorLength = + typeof existing.content === "string" ? existing.content.length : 0; + + return { + content: [ + { + type: "text" as const, + text: `Deleted thought ${id} (prior content length: ${priorLength} chars).`, + }, + ], + }; + } catch (err: unknown) { + return { + content: [ + { type: "text" as const, text: `Error: ${(err as Error).message}` }, + ], + isError: true, + }; + } + }, +); + +// --- Hono app with auth + CORS --- + +const corsHeaders = { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Headers": + "authorization, x-client-info, apikey, content-type, x-brain-key, accept, mcp-session-id", + "Access-Control-Allow-Methods": "GET, POST, OPTIONS, DELETE", +}; + +const app = new Hono(); + +app.options("*", (c) => c.text("ok", 200, corsHeaders)); + +app.all("*", async (c) => { + const provided = + c.req.header("x-brain-key") || new URL(c.req.url).searchParams.get("key"); + if (!provided || provided !== MCP_ACCESS_KEY) { + return c.json({ error: "Invalid or missing access key" }, 401, corsHeaders); + } + + if (!c.req.header("accept")?.includes("text/event-stream")) { + const headers = new Headers(c.req.raw.headers); + headers.set("Accept", "application/json, text/event-stream"); + const patched = new Request(c.req.raw.url, { + method: c.req.raw.method, + headers, + body: c.req.raw.body, + // @ts-ignore -- duplex required for streaming body in Deno + duplex: "half", + }); + Object.defineProperty(c.req, "raw", { value: patched, writable: true }); + } + + const transport = new StreamableHTTPTransport(); + await server.connect(transport); + return transport.handleRequest(c); +}); + +Deno.serve(app.fetch); diff --git a/integrations/delete-thought-mcp/metadata.json b/integrations/delete-thought-mcp/metadata.json new file mode 100644 index 00000000..869f1a23 --- /dev/null +++ b/integrations/delete-thought-mcp/metadata.json @@ -0,0 +1,20 @@ +{ + "name": "Delete Thought MCP", + "description": "Standalone MCP Edge Function that adds a delete_thought tool — hard-deletes a thought by UUID with a pre-flight fetch and a clear confirmation response.", + "category": "integrations", + "author": { + "name": "Scott Hutchinson", + "github": "txcfi-scott" + }, + "version": "1.0.0", + "requires": { + "open_brain": true, + "services": ["Supabase"], + "tools": ["Supabase CLI", "Deno"] + }, + "tags": ["mcp", "delete", "cleanup", "edge-function"], + "difficulty": "beginner", + "estimated_time": "10 minutes", + "created": "2026-04-23", + "updated": "2026-04-23" +}