From 7f6786642cc711bf4ab7c2457aa26c3888d3de18 Mon Sep 17 00:00:00 2001 From: Alan Shurafa Date: Sat, 13 Jun 2026 19:38:09 -0400 Subject: [PATCH 1/2] [integrations] Add Wiki MCP tools Edge Function Expose the persistent-wiki surface to any AI client without touching the core MCP server: wiki_list_pages, wiki_get_page, and a wiki_write_section that routes every write through the schema's regen guard so human-owned sections park a pending draft instead of being overwritten. Built as a remote Supabase Edge Function (per-request MCP construction, UUID page ids) following integrations/update-thought-mcp. Depends on the schemas/wiki-pages tables and RPC. Co-Authored-By: Claude Opus 4.8 (1M context) --- integrations/wiki-mcp/README.md | 141 ++++++++ integrations/wiki-mcp/deno.json | 5 + integrations/wiki-mcp/index.ts | 531 ++++++++++++++++++++++++++++ integrations/wiki-mcp/metadata.json | 20 ++ 4 files changed, 697 insertions(+) create mode 100644 integrations/wiki-mcp/README.md create mode 100644 integrations/wiki-mcp/deno.json create mode 100644 integrations/wiki-mcp/index.ts create mode 100644 integrations/wiki-mcp/metadata.json diff --git a/integrations/wiki-mcp/README.md b/integrations/wiki-mcp/README.md new file mode 100644 index 000000000..e5d63915a --- /dev/null +++ b/integrations/wiki-mcp/README.md @@ -0,0 +1,141 @@ +# Wiki MCP + +![Community Contribution](https://img.shields.io/badge/OB1_COMMUNITY-Approved_Contribution-2ea44f?style=for-the-badge&logo=github) + +**Created by [@alanshurafa](https://github.com/alanshurafa)** + +> Standalone MCP Edge Function that exposes the persistent-wiki tools — list pages, read a page with its sections, and write a section through the regen guard. + +## What It Does + +The core Open Brain MCP server captures and searches thoughts but knows nothing about wiki pages. This integration adds three tools as a separate Supabase Edge Function, registered as its own custom connector alongside your main Open Brain connector. + +The wiki itself lives in the `schemas/wiki-pages` schema: durable, revision-tracked pages whose sections each carry an owner. A section written by a machine (`origin='generated'`) can be regenerated freely. Once a human edits or locks a section, later machine writes stop overwriting it — they park as a pending draft for a person to review. That rule (the "regen guard") lives in the `wiki_write_section` database function, so every writer obeys it, including this one. + +The three tools: + +- **`wiki_list_pages`** — list active wiki pages, most recently updated first, each with a section count. Optional `page_kind` filter and `limit` / `offset` paging. Read-only. Use it to discover what pages exist. +- **`wiki_get_page`** — fetch one page by `slug`, including every section's `body_md` and any `pending_generated_md` draft. Read-only. Use it to read a page and to get the `page_id` (a UUID) and section keys you need to write. +- **`wiki_write_section`** — write or refresh a section, identified by `page_id` and `section_key`. It always writes as `origin='generated'` (agents are generators; this cannot be overridden). The database returns an `action`, which this tool surfaces verbatim: + - `action=created` — a new section was written. + - `action=updated` — an existing machine-owned section was refreshed in place. + - `action=pending` — the section is human-owned, so your text was parked as a pending draft for review. **Do not retry on `pending`** — the draft is already queued; a human accepts it separately. + +Why it matters: it lets any AI client keep a persistent wiki fresh without ever shredding prose a human has taken ownership of. The machine proposes; a human accepts. + +## Prerequisites + +- Working Open Brain setup ([guide](../../docs/01-getting-started.md)). +- The `schemas/wiki-pages` schema applied to your Open Brain database. This integration calls its tables (`wiki_pages`, `wiki_sections`) and its `wiki_write_section` RPC; without it the tools return errors. Apply that schema's `schema.sql` to your project first. +- Supabase CLI installed (`npm i -g supabase` or your preferred method). +- [Deno](https://deno.land/) available locally for type-checking (optional but recommended). + +No embedding provider or OpenRouter key is needed — these tools only read and write wiki rows; they do not generate embeddings. + +## Credential Tracker + +Copy this block into a text editor and fill it in as you go. + +```text +WIKI MCP -- CREDENTIAL TRACKER +------------------------------ + +FROM YOUR OPEN BRAIN SETUP + Project URL: ____________ + Service role key: ____________ + MCP access key: ____________ + +GENERATED DURING SETUP + Wiki MCP URL: https://.supabase.co/functions/v1/wiki-mcp + Custom connector name: Open Brain — Wiki + +------------------------------ +``` + +## Steps + +### 1. Apply the wiki-pages schema (if you have not already) + +This integration depends on the `schemas/wiki-pages` schema. Apply its `schema.sql` to your Open Brain project (via the Supabase SQL editor or `supabase db` tooling) before deploying this function. The schema is idempotent and safe to re-run. + +### 2. Create the Edge Function in your project + +From the root of your local Open Brain repo (the one you set up during getting-started): + +**1. Create the function folder:** + +```bash +supabase functions new wiki-mcp +``` + +**2. Copy the integration code:** + +```bash +curl -o supabase/functions/wiki-mcp/index.ts \ + https://raw.githubusercontent.com/NateBJones-Projects/OB1/main/integrations/wiki-mcp/index.ts +curl -o supabase/functions/wiki-mcp/deno.json \ + https://raw.githubusercontent.com/NateBJones-Projects/OB1/main/integrations/wiki-mcp/deno.json +``` + +### 3. Set environment variables + +Reuse the same access key as the core Open Brain server: + +```bash +supabase secrets set MCP_ACCESS_KEY="your-mcp-access-key" +``` + +`SUPABASE_URL` and `SUPABASE_SERVICE_ROLE_KEY` are injected automatically by the platform. + +### 4. Deploy + +```bash +supabase functions deploy wiki-mcp --no-verify-jwt +``` + +### 5. Register the connector in Claude Desktop + +Open **Settings → Connectors → Add custom connector** and paste: + +``` +https://.supabase.co/functions/v1/wiki-mcp?key= +``` + +Name it something distinct from your main Open Brain connector (e.g. `Open Brain — Wiki`) so the wiki tools show up clearly in your tool list. + +### 6. Verify + +Ask Claude to run the tools in sequence: + +1. `Call wiki_list_pages.` — you should see at least the `getting-started` seed page the schema creates. +2. `Call wiki_get_page with slug = "getting-started".` — note the `page_id` (a UUID) and the section keys. +3. `Call wiki_write_section with page_id = "", section_key = "intro", body_md = "Updated intro."` — because `intro` is a generated section, you should get `action=updated`. + +To see the regen guard park a draft: + +1. Edit a section so it becomes human-owned (set its `origin` to `manual`, or `locked` to true, in the database). +2. Call `wiki_write_section` against that section again. The result is now `action=pending` and the live text is left untouched — the draft waits in `pending_generated_md` for a human to accept. + +## Expected Outcome + +- A new Edge Function at `https://.supabase.co/functions/v1/wiki-mcp`. +- A custom connector registered in your AI client that exposes exactly three tools: `wiki_list_pages`, `wiki_get_page`, and `wiki_write_section`. +- Listing and reading pages returns wiki content with per-section ownership visible. +- Writing a machine-owned section refreshes it in place (`created` / `updated`); writing a human-owned section parks a pending draft (`pending`) without overwriting the live text, so the caller gets a clear "queued for review — do not retry" signal. + +The [MCP Tool Audit & Optimization Guide](../../docs/05-tool-audit.md) covers how to manage your tool surface area once you add this (and any other) custom connector. + +## Troubleshooting + +**Issue: Tool call returns an authentication error.** +Solution: Make sure the `?key=` parameter in your connector URL matches the `MCP_ACCESS_KEY` secret you set with `supabase secrets set`. If you rotate the key, re-deploy the function and update the connector URL. + +**Issue: `wiki_list_pages` / `wiki_get_page` errors mentioning a missing relation or function.** +Solution: The `schemas/wiki-pages` schema is not applied to this project. Apply its `schema.sql` (Step 1), then retry. + +**Issue: `wiki_write_section` keeps returning `action=pending`.** +Solution: This is by design. The target section is human-owned (`origin='manual'` or `locked`), so generated writes park as pending drafts rather than overwriting. Do not retry — a human accepts the draft (for example via the schema's `wiki_accept_pending` RPC), after which writes resume normally. + +## Attribution + +Ported from a multi-client persistent-wiki design so any Open Brain user can opt in to the wiki tools without touching the core server. diff --git a/integrations/wiki-mcp/deno.json b/integrations/wiki-mcp/deno.json new file mode 100644 index 000000000..5f87fd0cc --- /dev/null +++ b/integrations/wiki-mcp/deno.json @@ -0,0 +1,5 @@ +{ + "imports": { + "@supabase/supabase-js": "npm:@supabase/supabase-js@2.47.10" + } +} diff --git a/integrations/wiki-mcp/index.ts b/integrations/wiki-mcp/index.ts new file mode 100644 index 000000000..cba03e4ea --- /dev/null +++ b/integrations/wiki-mcp/index.ts @@ -0,0 +1,531 @@ +/** + * wiki-mcp — Standalone MCP Edge Function exposing the persistent-wiki tools. + * + * Adds three tools for the durable, human-override-safe wiki pages introduced + * by the `schemas/wiki-pages` schema: + * + * wiki_list_pages — enumerate active wiki pages (with section counts). + * wiki_get_page — fetch one page by slug, including all its sections and + * any pending generated drafts. + * wiki_write_section — write/refresh a section through the regen guard. The + * guard parks the write as a PENDING draft (rather than + * overwriting) when the target section is human-owned. + * + * Why a separate Edge Function? + * The core `open-brain` MCP server (server/index.ts) is curated and does not + * expose the wiki surface. This integration adds it without modifying the + * core server. Deploy it alongside your main connector and register it as a + * separate custom connector in Claude Desktop (Settings → Connectors → Add + * custom connector → paste URL). + * + * Depends on: the `schemas/wiki-pages` schema (tables `wiki_pages`, + * `wiki_sections`, `wiki_section_revisions` and the `wiki_write_section` + * RPC). Apply that schema before deploying this function. + * + * The regen guard (why writes can "park"): + * wiki_write_section always writes with origin='generated' — agents are + * generators, never owners. If the target section is human-owned + * (origin='manual' or locked), the DB RPC does NOT overwrite it; it parks the + * new text in the section's pending buffer and returns action='pending'. A + * human accepts the draft separately (wiki_accept_pending). This tool surfaces + * that action verbatim so the caller knows whether the write landed + * (created / updated) or parked for review (pending) — and must NOT retry on + * pending. + * + * ID contract: + * Open Brain ids are UUIDs. page_id is validated as a UUID string and passed + * through untouched — never parsed as a number. + * + * Auth: x-brain-key header OR ?key=... URL query parameter (same pattern as the + * core server — see server/index.ts). + * + * Env vars: + * SUPABASE_URL + * SUPABASE_SERVICE_ROLE_KEY + * MCP_ACCESS_KEY + */ + +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); + +// The actor recorded against generated writes and revisions in the wiki tables. +const WIKI_ACTOR = "wiki-mcp"; + +type WikiPageRow = { + id: string; + slug: string; + title: string; + page_kind: string; + status: string; + metadata: Record; + created_at: string; + updated_at: string; +}; + +type WikiSectionRow = { + id: string; + section_key: string; + heading: string | null; + display_order: number; + origin: string; + body_md: string; + pending_generated_md: string | null; + pending_generated_at: string | null; + generation_source: Record; + evidence_thought_ids: string[]; + locked: boolean; + created_at: string; + updated_at: string; +}; + +// --- MCP Server Setup (built per request — see #261) --- + +function buildServer(): McpServer { + const server = new McpServer({ + name: "open-brain-wiki", + version: "1.0.0", + }); + + // Tool 1: wiki_list_pages — enumerate available wiki pages. + server.registerTool( + "wiki_list_pages", + { + title: "Wiki List Pages", + description: + "List persistent wiki pages (topic summaries, entity profiles, etc.), most recently updated first, with a section count for each. Use this to discover available pages before fetching one with wiki_get_page. Only active pages are returned.", + annotations: { + readOnlyHint: true, + }, + inputSchema: { + page_kind: z + .string() + .max(40) + .optional() + .describe("Filter by page kind (e.g. topic, entity, autobiography, custom)"), + limit: z + .number() + .int() + .min(1) + .max(200) + .optional() + .default(50) + .describe("Max rows to return (default 50, max 200)"), + offset: z + .number() + .int() + .min(0) + .optional() + .default(0) + .describe("Pagination offset (default 0)"), + }, + }, + async ({ page_kind, limit, offset }) => { + try { + const lim = limit ?? 50; + const off = offset ?? 0; + + let q = supabase + .from("wiki_pages") + .select( + "id, slug, title, page_kind, status, metadata, created_at, updated_at", + ) + .eq("status", "active") + .order("updated_at", { ascending: false }) + .range(off, off + lim - 1); + + const kind = page_kind?.trim(); + if (kind) q = q.eq("page_kind", kind); + + const { data, error } = await q; + if (error) { + return { + content: [ + { type: "text" as const, text: `wiki_list_pages error: ${error.message}` }, + ], + isError: true, + }; + } + + const rows = (data ?? []) as WikiPageRow[]; + + // Count non-deleted sections per page in a single follow-up query. + const pageIds = rows.map((r) => r.id); + const sectionCounts: Record = {}; + if (pageIds.length > 0) { + const { data: scData, error: scErr } = await supabase + .from("wiki_sections") + .select("page_id") + .in("page_id", pageIds) + .is("deleted_at", null); + if (scErr) { + return { + content: [ + { + type: "text" as const, + text: `wiki_sections count error: ${scErr.message}`, + }, + ], + isError: true, + }; + } + for (const sc of (scData ?? []) as { page_id: string }[]) { + sectionCounts[sc.page_id] = (sectionCounts[sc.page_id] ?? 0) + 1; + } + } + + const pages = rows.map((r) => ({ + ...r, + section_count: sectionCounts[r.id] ?? 0, + })); + + const text = + pages.length === 0 + ? "No wiki pages found." + : pages + .map( + (p, i) => + `${off + i + 1}. [${p.page_kind}] ${p.slug} — ${p.title} ` + + `(${p.section_count} section${p.section_count === 1 ? "" : "s"}, id=${p.id})`, + ) + .join("\n"); + + return { + content: [{ type: "text" as const, text }], + structuredContent: { pages, count: pages.length, offset: off, limit: lim }, + }; + } catch (err: unknown) { + return { + content: [{ type: "text" as const, text: `Error: ${(err as Error).message}` }], + isError: true, + }; + } + }, + ); + + // Tool 2: wiki_get_page — fetch one page by slug, with all its sections. + server.registerTool( + "wiki_get_page", + { + title: "Wiki Get Page", + description: + "Fetch a wiki page by slug, including all of its sections (body_md and any pending_generated_md draft). Use this to read a page and to obtain its page_id and section keys before calling wiki_write_section. pending_generated_md is an agent-generated draft awaiting human acceptance on a human-owned section.", + annotations: { + readOnlyHint: true, + }, + inputSchema: { + slug: z + .string() + .min(1) + .max(200) + .describe("Page slug (the URL-safe identifier shown by wiki_list_pages)"), + }, + }, + async ({ slug }) => { + try { + const wantedSlug = slug.trim(); + if (!wantedSlug) { + return { + content: [{ type: "text" as const, text: "slug is required" }], + isError: true, + }; + } + + const { data: page, error: pageErr } = await supabase + .from("wiki_pages") + .select( + "id, slug, title, page_kind, status, metadata, created_at, updated_at", + ) + .eq("slug", wantedSlug) + .maybeSingle(); + + if (pageErr) { + return { + content: [ + { type: "text" as const, text: `wiki_get_page error: ${pageErr.message}` }, + ], + isError: true, + }; + } + if (!page) { + return { + content: [ + { type: "text" as const, text: `Wiki page '${wantedSlug}' not found.` }, + ], + isError: true, + }; + } + + const pageRow = page as WikiPageRow; + + const { data: sections, error: secErr } = await supabase + .from("wiki_sections") + .select( + "id, section_key, heading, display_order, origin, body_md, pending_generated_md, pending_generated_at, generation_source, evidence_thought_ids, locked, created_at, updated_at", + ) + .eq("page_id", pageRow.id) + .is("deleted_at", null) + .order("display_order", { ascending: true }); + + if (secErr) { + return { + content: [ + { type: "text" as const, text: `wiki_sections error: ${secErr.message}` }, + ], + isError: true, + }; + } + + const sectionRows = (sections ?? []) as WikiSectionRow[]; + const sectionLines = sectionRows.map( + (s) => + ` [${s.section_key}] ${s.heading ?? "(no heading)"} ` + + `(origin=${s.origin}${s.locked ? ", locked" : ""}` + + `${s.pending_generated_md ? ", has pending draft" : ""})`, + ); + + const text = [ + `[${pageRow.page_kind}] ${pageRow.slug}: ${pageRow.title} (id=${pageRow.id})`, + `Sections (${sectionRows.length}):`, + ...(sectionLines.length ? sectionLines : [" (none)"]), + ].join("\n"); + + return { + content: [{ type: "text" as const, text }], + structuredContent: { page: pageRow, sections: sectionRows }, + }; + } catch (err: unknown) { + return { + content: [{ type: "text" as const, text: `Error: ${(err as Error).message}` }], + isError: true, + }; + } + }, + ); + + // Tool 3: wiki_write_section — write/refresh a section through the regen guard. + server.registerTool( + "wiki_write_section", + { + title: "Wiki Write Section", + description: + "Write or refresh a wiki section. Always writes as origin='generated' (agents are generators — this is enforced and cannot be overridden). IMPORTANT: if the target section is human-owned (origin='manual' or locked), the write does NOT overwrite it — it is parked as a pending draft and action='pending' is returned. Do NOT retry when action='pending'; the draft is queued for a human to review and accept. action='created' or action='updated' means the write was applied. Identify the section with page_id (a UUID, from wiki_get_page) and section_key.", + annotations: { + readOnlyHint: false, + destructiveHint: false, + idempotentHint: false, + openWorldHint: false, + }, + inputSchema: { + page_id: z + .string() + .uuid() + .describe("Wiki page UUID (from wiki_get_page or wiki_list_pages)"), + section_key: z + .string() + .min(1) + .max(120) + .describe("Section key, e.g. 'summary', 'context', 'notes'"), + body_md: z + .string() + .max(50_000) + .describe("Markdown body for the section"), + heading: z + .string() + .max(500) + .optional() + .describe("Optional section heading text"), + }, + }, + async ({ page_id, section_key, body_md, heading }) => { + try { + const pageId = page_id.trim(); + const sectionKey = section_key.trim(); + if (!sectionKey) { + return { + content: [{ type: "text" as const, text: "section_key is required" }], + isError: true, + }; + } + + // The regen guard, origin enforcement, and revision snapshotting all + // live in the wiki_write_section RPC — a single DB-side guard every + // writer shares. We always pass p_origin='generated'. + const rpcParams: Record = { + p_page_id: pageId, + p_section_key: sectionKey, + p_body_md: body_md, + p_origin: "generated", + p_actor: WIKI_ACTOR, + }; + const trimmedHeading = heading?.trim(); + if (trimmedHeading) rpcParams.p_heading = trimmedHeading; + + const { data, error } = await supabase.rpc("wiki_write_section", rpcParams); + if (error) { + return { + content: [ + { type: "text" as const, text: `wiki_write_section error: ${error.message}` }, + ], + isError: true, + }; + } + + const result = + data && typeof data === "object" + ? (data as Record) + : { result: data }; + const action = typeof result.action === "string" ? result.action : "unknown"; + const sectionId = typeof result.section_id === "string" ? result.section_id : null; + + // Surface the regen-guard outcome verbatim so the caller knows whether + // the write landed or parked for human review. + const actionMessage = + action === "pending" + ? "action=pending — section is human-owned; your draft was parked for human review. Do NOT retry." + : action === "created" + ? "action=created — new section written." + : action === "updated" + ? "action=updated — section refreshed in place." + : `action=${action}.`; + + const text = + `wiki_write_section [${sectionKey}] on page ${pageId}: ${actionMessage}` + + (sectionId ? `\n · section_id: ${sectionId}` : ""); + + return { + content: [{ type: "text" as const, text }], + structuredContent: { + ...result, + page_id: pageId, + section_key: sectionKey, + origin: "generated", + }, + }; + } catch (err: unknown) { + return { + content: [{ type: "text" as const, text: `Error: ${(err as Error).message}` }], + isError: true, + }; + } + }, + ); + + return server; +} + +// --- 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, mcp-protocol-version, last-event-id", + "Access-Control-Allow-Methods": "GET, POST, OPTIONS, DELETE", +}; + +// JSON-RPC error code for unauthorized requests. Per the JSON-RPC 2.0 spec the +// -32099..-32000 range is reserved for implementation-defined server errors; +// -32001 is the conventional "Unauthorized" code used by MCP clients/servers. +// +// We return a JSON-RPC envelope (HTTP 200) instead of a bare HTTP 401 because +// strict MCP hosts (Codex CLI, Claude Code) treat bare HTTP 4xx responses as +// transport-level failures and tear the connection down rather than surfacing +// the error to the application. Wrapping the rejection keeps the connection +// alive so clients can recover (e.g. prompt for a new key). +const JSON_RPC_UNAUTHORIZED_CODE = -32001; +const UNAUTHORIZED_MESSAGE = "Unauthorized: missing or invalid authentication."; + +async function readBodyText(req: Request): Promise { + if (req.method === "GET" || req.method === "HEAD" || req.method === "DELETE") { + return null; + } + try { + return await req.text(); + } catch { + return null; + } +} + +function extractJsonRpcId(bodyText: string | null): string | number | null { + if (!bodyText) return null; + try { + const parsed = JSON.parse(bodyText); + if (parsed && typeof parsed === "object" && "id" in parsed) { + const id = (parsed as { id: unknown }).id; + if (typeof id === "string" || typeof id === "number" || id === null) { + return id; + } + } + } catch { + // fall through — malformed body + } + return null; +} + +function unauthorizedResponse(id: string | number | null): Response { + const body = { + jsonrpc: "2.0", + error: { code: JSON_RPC_UNAUTHORIZED_CODE, message: UNAUTHORIZED_MESSAGE }, + id, + }; + return new Response(JSON.stringify(body), { + status: 200, + headers: { "Content-Type": "application/json", ...corsHeaders }, + }); +} + +const app = new Hono(); + +// CORS preflight — required for browser/Electron-based clients (Claude Desktop). +app.options("*", (c) => c.text("ok", 200, corsHeaders)); + +app.all("*", async (c) => { + // Accept the access key via header OR URL query parameter. + const provided = + c.req.header("x-brain-key") || new URL(c.req.url).searchParams.get("key"); + if (!provided || provided !== MCP_ACCESS_KEY) { + const bodyText = await readBodyText(c.req.raw); + const id = extractJsonRpcId(bodyText); + return unauthorizedResponse(id); + } + + // Claude Desktop connectors don't send the Accept header that + // StreamableHTTPTransport requires. Patch it in when missing. + 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 }); + } + + // Build the MCP server per request (no shared singleton) — matches the core + // server's per-request construction landed in #261. + const server = buildServer(); + const transport = new StreamableHTTPTransport(); + await server.connect(transport); + const response = await transport.handleRequest(c); + if (!response) { + return c.json({ error: "No response from MCP transport" }, 500, corsHeaders); + } + response.headers.delete("mcp-session-id"); + for (const [k, v] of Object.entries(corsHeaders)) response.headers.set(k, v); + return response; +}); + +Deno.serve(app.fetch); diff --git a/integrations/wiki-mcp/metadata.json b/integrations/wiki-mcp/metadata.json new file mode 100644 index 000000000..c7b46eb06 --- /dev/null +++ b/integrations/wiki-mcp/metadata.json @@ -0,0 +1,20 @@ +{ + "name": "Wiki MCP", + "description": "Standalone MCP Edge Function exposing the persistent-wiki tools: wiki_list_pages, wiki_get_page, and a wiki_write_section that routes every write through the regen guard so human-owned sections park a pending draft instead of being overwritten.", + "category": "integrations", + "author": { + "name": "Alan Shurafa", + "github": "alanshurafa" + }, + "version": "1.0.0", + "requires": { + "open_brain": true, + "services": ["Supabase", "schemas/wiki-pages"], + "tools": ["Supabase CLI", "Deno"] + }, + "tags": ["mcp", "wiki", "edge-function", "regen-guard", "human-in-the-loop"], + "difficulty": "intermediate", + "estimated_time": "15 minutes", + "created": "2026-06-13", + "updated": "2026-06-13" +} From cfa83cee2d90b345045edc27a599fe986ea2ee0e Mon Sep 17 00:00:00 2001 From: Alan Shurafa Date: Sun, 14 Jun 2026 16:12:58 -0400 Subject: [PATCH 2/2] [integrations] Map MCP and Hono imports in wiki-mcp deno.json The committed import map only declared @supabase/supabase-js, so copying deno.json + index.ts into a Supabase function left @hono/mcp, @modelcontextprotocol/sdk, hono, and zod as unresolved bare specifiers and bundling failed. Mirror the canonical versions from server/deno.json. Resolving the imports surfaced a real type error: deno check now reaches the wiki_write_section result-narrowing, where the {result: data} union branch lacks .action/.section_id. Annotate result as Record so the typecheck passes clean. Add a "More from Nate" provenance CTA to the README. Co-Authored-By: Claude Opus 4.8 (1M context) --- integrations/wiki-mcp/README.md | 4 ++++ integrations/wiki-mcp/deno.json | 4 ++++ integrations/wiki-mcp/index.ts | 2 +- 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/integrations/wiki-mcp/README.md b/integrations/wiki-mcp/README.md index e5d63915a..ad9a64fd2 100644 --- a/integrations/wiki-mcp/README.md +++ b/integrations/wiki-mcp/README.md @@ -139,3 +139,7 @@ Solution: This is by design. The target section is human-owned (`origin='manual' ## Attribution Ported from a multi-client persistent-wiki design so any Open Brain user can opt in to the wiki tools without touching the core server. + +## More from Nate + +Open Brain is built in the open by Nate B. Jones — more practical systems like this on his [Substack](https://substack.com/@natesnewsletter) and at [natebjones.com](https://natebjones.com). diff --git a/integrations/wiki-mcp/deno.json b/integrations/wiki-mcp/deno.json index 5f87fd0cc..705b50447 100644 --- a/integrations/wiki-mcp/deno.json +++ b/integrations/wiki-mcp/deno.json @@ -1,5 +1,9 @@ { "imports": { + "@hono/mcp": "npm:@hono/mcp@0.1.1", + "@modelcontextprotocol/sdk": "npm:@modelcontextprotocol/sdk@1.24.3", + "hono": "npm:hono@4.9.2", + "zod": "npm:zod@4.1.13", "@supabase/supabase-js": "npm:@supabase/supabase-js@2.47.10" } } diff --git a/integrations/wiki-mcp/index.ts b/integrations/wiki-mcp/index.ts index cba03e4ea..94eed8ccc 100644 --- a/integrations/wiki-mcp/index.ts +++ b/integrations/wiki-mcp/index.ts @@ -381,7 +381,7 @@ function buildServer(): McpServer { }; } - const result = + const result: Record = data && typeof data === "object" ? (data as Record) : { result: data };