Memex
Setup Guide
Your thoughts and context, owned by you — not your AI vendor. Store once, access from Claude, ChatGPT, Cursor, or whatever comes next. One brain, every tool.
Based on Nate Jones' Open Brain guide.
Supabase Free
Postgres database with pgvector for storing thoughts, embeddings, and metadata.
OpenRouter ~$0.20/mo
AI gateway. Generates embeddings (text-embedding-3-small) and extracts metadata (gpt-4o-mini).
MCP Server Hosted
A Supabase Edge Function exposing 4 tools: search, list, stats, and capture — accessible from any AI.
AI Clients
Claude Code, Claude Desktop, ChatGPT, Cursor — any MCP-compatible client connects via a single URL.
Database Setup
Create your Supabase project and configure the vector database.
Create Your Supabase Project
Set up the free database that stores all your thoughts.
- Go to supabase.com and sign up (GitHub login is fastest)
- Click New Project in the dashboard
- Set the project name to
memex - Generate a strong Database password → save it in your password manager
- Pick your closest region
- Click Create new project and wait 1–2 minutes
- Find your Project ref in the dashboard URL (e.g.
abcdefghijklmnop) → save it
supabase.com/dashboard/project/YOUR_REF
Set Up the Database
Enable vector search and create the thoughts table + search function.
- In Supabase, go to Database → Extensions
- Search for
vector - Flip pgvector ON
- Go to SQL Editor in the left sidebar
- Click New query
- Paste the entire block below and click Run
-- Create the thoughts table
create table thoughts (
id uuid default gen_random_uuid() primary key,
content text not null,
embedding vector(1536),
metadata jsonb default '{}'::jsonb,
created_at timestamptz default now(),
updated_at timestamptz default now()
);
-- Indexes for fast search
create index on thoughts using hnsw (embedding vector_cosine_ops);
create index on thoughts using gin (metadata);
create index on thoughts (created_at desc);
-- Auto-update updated_at timestamp
create or replace function update_updated_at()
returns trigger as $$
begin
new.updated_at = now();
return new;
end;
$$ language plpgsql;
create trigger thoughts_updated_at
before update on thoughts
for each row execute function update_updated_at();
-- Semantic search function
create or replace function match_thoughts(
query_embedding vector(1536),
match_threshold float default 0.7,
match_count int default 10,
filter jsonb default '{}'::jsonb
)
returns table (
id uuid,
content text,
metadata jsonb,
similarity float,
created_at timestamptz
)
language plpgsql as $$
begin
return query
select
t.id,
t.content,
t.metadata,
1 - (t.embedding <=> query_embedding) as similarity,
t.created_at
from thoughts t
where 1 - (t.embedding <=> query_embedding) > match_threshold
and (filter = '{}'::jsonb or t.metadata @> filter)
order by t.embedding <=> query_embedding
limit match_count;
end;
$$;
-- Lock down security (service role only)
alter table thoughts enable row level security;
create policy "Service role full access"
on thoughts for all
using (auth.role() = 'service_role');
- Table Editor should show a
thoughtstable with columns: id, content, embedding, metadata, created_at, updated_at - Database → Functions should show
match_thoughts
Save Your Supabase Credentials
You'll need these throughout setup.
- In Supabase, go to Settings (gear icon) → API
- Copy your Project URL → save it somewhere safe
- Copy the Secret key (also called "service_role key") → save it somewhere safe
Get an OpenRouter API Key
OpenRouter powers both embeddings and metadata extraction.
- Go to openrouter.ai and sign up
- Navigate to openrouter.ai/keys
- Click Create Key, name it
memex - Copy the key immediately (it won't be shown again) → save it somewhere safe
- Go to Credits and add $5 — this will last months at typical usage
text-embedding-3-small ~$0.02/million tokens · gpt-4o-mini ~$0.15/million input tokens. For 20 thoughts/day: roughly $0.10–0.30/month.
MCP Server
Deploy a hosted MCP server that gives any AI access to your brain.
Install the Supabase CLI & Link Your Project
You'll use this to deploy Edge Functions from your computer.
- Windows: Press Windows key + R, type
powershell, press Enter. A blue/black window will open — this is PowerShell, your terminal. - Mac: Open Spotlight (Cmd + Space), type
terminal, press Enter.
- Type or paste the command for your system below, then press Enter. It will download and install the Supabase CLI tool.
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
brew install supabase/tap/supabase
supabase --version
- You should see a version number like
2.x.x. If so, it worked.
- Run these two commands one at a time. The first will open a browser to log in to Supabase. The second connects your terminal to your specific project.
supabase login
supabase link --project-ref YOUR_PROJECT_REF
YOUR_PROJECT_REF with the value you saved in Step 1 — it's the string of letters/numbers in your Supabase dashboard URL after /project/.
Generate an Access Key
This key secures your MCP server — only clients with it can connect.
openssl rand -hex 32
-join ((1..32) | ForEach-Object { '{0:x2}' -f (Get-Random -Maximum 256) })
- Copy the 64-character output → save it somewhere safe as your MCP Access Key
supabase secrets set MCP_ACCESS_KEY=your-generated-key-here
supabase secrets set OPENROUTER_API_KEY=your-openrouter-key-here
SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY are automatically available inside all Edge Functions — no need to set them manually.
Deploy the MCP Server
A Supabase Edge Function with 4 brain tools: search, list, stats, capture.
supabase functions new memex-mcp
- Create the file
supabase/functions/memex-mcp/deno.jsonwith this content:
{
"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"
}
}
- Replace the entire contents of
supabase/functions/memex-mcp/index.tswith the code below:
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 OPENROUTER_API_KEY = Deno.env.get("OPENROUTER_API_KEY")!;
const MCP_ACCESS_KEY = Deno.env.get("MCP_ACCESS_KEY")!;
const OPENROUTER_BASE = "https://openrouter.ai/api/v1";
const supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY);
async function getEmbedding(text: string): Promise<number[]> {
const r = await fetch(`${OPENROUTER_BASE}/embeddings`, {
method: "POST",
headers: {
Authorization: `Bearer ${OPENROUTER_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ model: "openai/text-embedding-3-small", input: text }),
});
if (!r.ok) {
const msg = await r.text().catch(() => "");
throw new Error(`OpenRouter embeddings failed: ${r.status} ${msg}`);
}
const d = await r.json();
return d.data[0].embedding;
}
async function extractMetadata(text: string): Promise<Record<string, unknown>> {
const r = await fetch(`${OPENROUTER_BASE}/chat/completions`, {
method: "POST",
headers: {
Authorization: `Bearer ${OPENROUTER_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "openai/gpt-4o-mini",
response_format: { type: "json_object" },
messages: [
{
role: "system",
content: `Extract metadata from the user's captured thought. Return JSON with:
- "people": array of people mentioned (empty if none)
- "action_items": array of implied to-dos (empty if none)
- "dates_mentioned": array of dates YYYY-MM-DD (empty if none)
- "topics": array of 1-3 short topic tags (always at least one)
- "type": one of "observation", "task", "idea", "reference", "person_note"
Only extract what's explicitly there.`,
},
{ role: "user", content: text },
],
}),
});
const d = await r.json();
try {
return JSON.parse(d.choices[0].message.content);
} catch {
return { topics: ["uncategorized"], type: "observation" };
}
}
// ── MCP Server ──────────────────────────────────────────────────────────────
const server = new McpServer({ name: "memex", version: "1.0.0" });
// Tool 1: Semantic Search
server.registerTool(
"search_thoughts",
{
title: "Search Thoughts",
description:
"Search captured thoughts by meaning. Use when the user asks about a topic, person, or idea they've previously captured.",
inputSchema: {
query: z.string().describe("What to search for"),
limit: z.number().optional().default(10),
threshold: z.number().optional().default(0.5),
},
},
async ({ query, limit, threshold }) => {
try {
const qEmb = await getEmbedding(query);
const { data, error } = await supabase.rpc("match_thoughts", {
query_embedding: qEmb,
match_threshold: threshold,
match_count: limit,
filter: {},
});
if (error) return { content: [{ type: "text" as const, text: `Search error: ${error.message}` }], isError: true };
if (!data || data.length === 0)
return { content: [{ type: "text" as const, text: `No thoughts found matching "${query}".` }] };
const results = data.map((t: any, i: number) => {
const m = t.metadata || {};
const parts = [
`--- Result ${i + 1} (${(t.similarity * 100).toFixed(1)}% match) ---`,
`Captured: ${new Date(t.created_at).toLocaleDateString()}`,
`Type: ${m.type || "unknown"}`,
];
if (Array.isArray(m.topics) && m.topics.length) parts.push(`Topics: ${m.topics.join(", ")}`);
if (Array.isArray(m.people) && m.people.length) parts.push(`People: ${m.people.join(", ")}`);
if (Array.isArray(m.action_items) && m.action_items.length) parts.push(`Actions: ${m.action_items.join("; ")}`);
parts.push(`\n${t.content}`);
return parts.join("\n");
});
return { content: [{ type: "text" as const, text: `Found ${data.length} thought(s):\n\n${results.join("\n\n")}` }] };
} catch (err: unknown) {
return { content: [{ type: "text" as const, text: `Error: ${(err as Error).message}` }], isError: true };
}
}
);
// Tool 2: List Recent
server.registerTool(
"list_thoughts",
{
title: "List Recent Thoughts",
description: "List recently captured thoughts with optional filters by type, topic, person, or time range.",
inputSchema: {
limit: z.number().optional().default(10),
type: z.string().optional().describe("Filter by type: observation, task, idea, reference, person_note"),
topic: z.string().optional().describe("Filter by topic tag"),
person: z.string().optional().describe("Filter by person mentioned"),
days: z.number().optional().describe("Only thoughts from the last N days"),
},
},
async ({ limit, type, topic, person, days }) => {
try {
let q = supabase.from("thoughts").select("content, metadata, created_at")
.order("created_at", { ascending: false }).limit(limit);
if (type) q = q.contains("metadata", { type });
if (topic) q = q.contains("metadata", { topics: [topic] });
if (person) q = q.contains("metadata", { people: [person] });
if (days) {
const since = new Date();
since.setDate(since.getDate() - days);
q = q.gte("created_at", since.toISOString());
}
const { data, error } = await q;
if (error) return { content: [{ type: "text" as const, text: `Error: ${error.message}` }], isError: true };
if (!data || !data.length) return { content: [{ type: "text" as const, text: "No thoughts found." }] };
const results = data.map((t: any, i: number) => {
const m = t.metadata || {};
const tags = Array.isArray(m.topics) ? m.topics.join(", ") : "";
return `${i + 1}. [${new Date(t.created_at).toLocaleDateString()}] (${m.type || "??"}${tags ? " - " + tags : ""})\n ${t.content}`;
});
return { content: [{ type: "text" as const, text: `${data.length} recent thought(s):\n\n${results.join("\n\n")}` }] };
} catch (err: unknown) {
return { content: [{ type: "text" as const, text: `Error: ${(err as Error).message}` }], isError: true };
}
}
);
// Tool 3: Stats
server.registerTool(
"thought_stats",
{
title: "Thought Statistics",
description: "Get a summary of all captured thoughts: totals, types, top topics, and people.",
inputSchema: {},
},
async () => {
try {
const { count } = await supabase.from("thoughts").select("*", { count: "exact", head: true });
const { data } = await supabase.from("thoughts").select("metadata, created_at").order("created_at", { ascending: false });
const types: Record<string, number> = {};
const topics: Record<string, number> = {};
const people: Record<string, number> = {};
for (const r of data || []) {
const m = (r.metadata || {}) as Record<string, unknown>;
if (m.type) types[m.type as string] = (types[m.type as string] || 0) + 1;
if (Array.isArray(m.topics)) for (const t of m.topics) topics[t] = (topics[t] || 0) + 1;
if (Array.isArray(m.people)) for (const p of m.people) people[p] = (people[p] || 0) + 1;
}
const sort = (o: Record<string, number>) => Object.entries(o).sort((a, b) => b[1] - a[1]).slice(0, 10);
const lines = [
`Total thoughts: ${count}`,
data?.length
? `Date range: ${new Date(data[data.length - 1].created_at).toLocaleDateString()} → ${new Date(data[0].created_at).toLocaleDateString()}`
: "Date range: N/A",
"",
"Types:",
...sort(types).map(([k, v]) => ` ${k}: ${v}`),
];
if (Object.keys(topics).length) { lines.push("", "Top topics:"); for (const [k, v] of sort(topics)) lines.push(` ${k}: ${v}`); }
if (Object.keys(people).length) { lines.push("", "People mentioned:"); for (const [k, v] of sort(people)) lines.push(` ${k}: ${v}`); }
return { content: [{ type: "text" as const, text: lines.join("\n") }] };
} catch (err: unknown) {
return { content: [{ type: "text" as const, text: `Error: ${(err as Error).message}` }], isError: true };
}
}
);
// Tool 4: Capture Thought
server.registerTool(
"capture_thought",
{
title: "Capture Thought",
description:
"Save a new thought to the Memex. Generates an embedding and extracts metadata automatically. Use when the user wants to save notes, insights, decisions, or memories directly from any AI client.",
inputSchema: {
content: z.string().describe("The thought to capture — a clear, standalone statement that will make sense when retrieved later"),
},
},
async ({ content }) => {
try {
const [embedding, metadata] = await Promise.all([getEmbedding(content), extractMetadata(content)]);
const { error } = await supabase.from("thoughts").insert({ content, embedding, metadata: { ...metadata, source: "mcp" } });
if (error) return { content: [{ type: "text" as const, text: `Failed to capture: ${error.message}` }], isError: true };
const meta = metadata as Record<string, unknown>;
let confirmation = `Captured as ${meta.type || "thought"}`;
if (Array.isArray(meta.topics) && meta.topics.length) confirmation += ` — ${meta.topics.join(", ")}`;
if (Array.isArray(meta.people) && meta.people.length) confirmation += ` | People: ${meta.people.join(", ")}`;
if (Array.isArray(meta.action_items) && meta.action_items.length) confirmation += ` | Actions: ${meta.action_items.join("; ")}`;
return { content: [{ type: "text" as const, text: confirmation }] };
} catch (err: unknown) {
return { content: [{ type: "text" as const, text: `Error: ${(err as Error).message}` }], isError: true };
}
}
);
// ── Hono App with Auth ────────────────────────────────────────────────────────
const app = new Hono();
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);
}
const transport = new StreamableHTTPTransport();
await server.connect(transport);
return transport.handleRequest(c);
});
Deno.serve(app.fetch);
supabase functions deploy memex-mcp --no-verify-jwt
https://YOUR_PROJECT_REF.supabase.co/functions/v1/memex-mcpYour connection URL (with key):
https://YOUR_PROJECT_REF.supabase.co/functions/v1/memex-mcp?key=YOUR_ACCESS_KEY
Connect & Use
Add the MCP server to your AI clients.
Connect to Your AI
Pick any client below — you only need one to get started.
claude mcp add --transport http memex \
https://YOUR_PROJECT_REF.supabase.co/functions/v1/memex-mcp \
--header "x-brain-key: YOUR_ACCESS_KEY"
- Open Claude Desktop → Settings → Connectors
- Click Add custom connector
- Name:
Memex - Remote MCP server URL: paste your full connection URL (ending in
?key=...) - Click Add
- Enable Developer Mode (one-time): Profile → Settings → Apps & Connectors → Advanced settings → Developer mode ON
- In Settings → Apps & Connectors, click Create
- Name:
Memex· Authentication: No Authentication - MCP endpoint URL: paste your full connection URL (ending in
?key=...) - Click Create
{
"mcpServers": {
"memex": {
"command": "npx",
"args": [
"mcp-remote",
"https://YOUR_PROJECT_REF.supabase.co/functions/v1/memex-mcp",
"--header",
"x-brain-key:${BRAIN_KEY}"
],
"env": {
"BRAIN_KEY": "YOUR_ACCESS_KEY"
}
}
}
}
x-brain-key:${BRAIN_KEY}
Run this in your terminal (outside of opencode):
opencode mcp add
- Name:
memex - Type: Remote
- URL: paste your full connection URL (ending in
?key=...) - OAuth: No
OpenCode writes the config to ~/.config/opencode/opencode.json automatically.
Use It
Your AI automatically picks the right tool based on what you say.
Security Notes
What's protected and what to be aware of.
| Area | Status | Details |
|---|---|---|
| MCP access key | Protected | All requests require your key via header or URL param. 401 returned otherwise. |
| Database | Protected | Row Level Security enabled. Only service role key (stored as Supabase secret) can access data. |
| Input limits | Enforced | Content capped at 50,000 chars. Query at 2,000. Result limits capped at 50. Prevents API credit exhaustion. |
| Key in URL | Low risk | When using ?key= (ChatGPT/Claude Desktop), the key appears in Supabase's server logs. Use the x-brain-key header method where possible (Claude Code does this by default). |
| Rate limiting | None | No rate limiting on the endpoint. If your key is leaked, someone could run up your OpenRouter bill. Rotate your key via supabase secrets set if you suspect exposure. |
| Secrets storage | Protected | All keys stored as Supabase secrets — never in code or git. Edge Function reads them at runtime only. |
supabase secrets set MCP_ACCESS_KEY=new-key, redeploy the function, and update your AI clients.
Cost Breakdown
Estimated for 20 thoughts/day.
| Service | What it does | Cost |
|---|---|---|
| Supabase | Database + Edge Functions hosting | Free tier |
| text-embedding-3-small | Converts thoughts to searchable vectors | ~$0.02 / million tokens |
| gpt-4o-mini | Extracts topics, people, action items | ~$0.15 / million input tokens |
| Total (20 thoughts/day) | $0.10 – $0.30 / month |