Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion extensions/_template/AGENT_SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,11 @@ const server = new McpServer({
const app = new Hono();

app.all("*", async (c) => {
const provided = c.req.header("x-brain-key") || new URL(c.req.url).searchParams.get("key");
// Accept access key via Authorization: Bearer header, x-brain-key header, or URL query parameter
const authHeader = c.req.header("Authorization");
const provided = (authHeader?.startsWith("Bearer ") && authHeader.split(" ")[1])
|| 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);
}
Expand Down
6 changes: 5 additions & 1 deletion integrations/kubernetes-deployment/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -415,7 +415,11 @@ server.registerTool(
const app = new Hono();

app.all("*", async (c) => {
const provided = c.req.header("x-brain-key") || new URL(c.req.url).searchParams.get("key");
// Accept access key via Authorization: Bearer header, x-brain-key header, or URL query parameter
const authHeader = c.req.header("Authorization");
const provided = (authHeader?.startsWith("Bearer ") && authHeader.split(" ")[1])
|| 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);
}
Expand Down
61 changes: 61 additions & 0 deletions integrations/perplexity-pro/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Perplexity Pro MCP

> Connect Perplexity Pro as an MCP client to your Open Brain — enables AI-powered search across your captured thoughts.

## What It Does

Perplexity Pro can connect to remote MCP servers. Once connected, you can search your Open Brain directly from Perplexity using natural language. Your thoughts become part of Perplexity's contextual awareness.

## Prerequisites

- Working Open Brain setup ([guide](../../docs/01-getting-started.md))
- Perplexity Pro subscription
- Updated Open Brain MCP server (with Bearer auth support)

## Steps

### 1. Update Your MCP Server

If you deployed Open Brain before April 2026, update the edge function:

```bash
cd your-supabase-project
supabase functions deploy open-brain-mcp --no-verify-jwt
```

### 2. Get Your Connection Details

- **MCP URL:** `https://YOUR_PROJECT_REF.supabase.co/functions/v1/open-brain-mcp`
- **Access Key:** Your MCP_ACCESS_KEY from Step 5 of the setup guide

### 3. Connect Perplexity

1. Open Perplexity Pro > Your user profile > settings
2. Navigate to MCP / Connectors
3. Add new connector:
- **Name:** MyOpenBrain
- **URL:** `https://YOUR_PROJECT_REF.supabase.co/functions/v1/open-brain-mcp`
- **Authentication:** Choose API `Authorization: Bearer YOUR_ACCESS_KEY`
4. Save and enable it

### 4. Verify Connection

From Perplexity input field, click "+" and choose the MyOpenBrain connector.

Ask Perplexity: "What thoughts do I have about [topic]?"

## Expected Outcome

Perplexity returns relevant thoughts from your Open Brain with citations. The four MCP tools (search_thoughts, list_thoughts, thought_stats, capture_thought) are available.

## Troubleshooting

**Issue: "Couldn't connect to MCP server"**
Solution: Ensure your MCP server is deployed and supports Bearer auth. Re-deploy if needed.

**Issue: "Invalid access key"**
Solution: Verify the key matches your MCP_ACCESS_KEY exactly. Re-check in Supabase secrets.

## Tool Surface Area

This integration adds 4 MCP tools to your Perplexity context. See the [MCP Tool Audit & Optimization Guide](../../docs/05-tool-audit.md).
20 changes: 20 additions & 0 deletions integrations/perplexity-pro/metadata.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "Perplexity Pro MCP",
"description": "Connect Perplexity Pro as an MCP client to search and capture thoughts in Open Brain.",
"category": "integrations",
"author": {
"name": "Antonio De Marinis",
"github": "demarant"
},
"version": "1.0.0",
"requires": {
"open_brain": true,
"services": ["Perplexity Pro"],
"tools": []
},
"tags": ["perplexity", "mcp", "client", "search"],
"difficulty": "beginner",
"estimated_time": "10 minutes",
"created": "2026-04-04",
"updated": "2026-04-04"
}
22 changes: 11 additions & 11 deletions recipes/fingerprint-dedup-backfill/backfill-fingerprints.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -94,14 +94,14 @@ function buildContentFingerprint(text) {

// ── REST helpers ────────────────────────────────────────────────────────────

async function fetchBatch(cursorId, batchSize) {
async function fetchBatch(cursorCreatedAt, batchSize) {
const url =
`${REST_BASE}/thoughts` +
`?content_fingerprint=is.null` +
`&id=gt.${cursorId}` +
`&select=id,content` +
`&created_at=gt.${encodeURIComponent(cursorCreatedAt)}` +
`&select=id,content,created_at` +
`&limit=${batchSize}` +
`&order=id.asc`;
`&order=created_at.asc`;
const res = await fetch(url, { headers: HEADERS });
if (!res.ok) {
const body = await res.text().catch(() => "");
Expand Down Expand Up @@ -162,7 +162,7 @@ function loadState() {
return JSON.parse(fs.readFileSync(STATE_FILE, "utf8"));
} catch {
return {
cursorId: 0,
cursorCreatedAt: "1970-01-01T00:00:00Z",
totalDone: 0,
totalDuplicates: 0,
totalErrors: 0,
Expand All @@ -183,20 +183,20 @@ async function main() {
const state = loadState();
console.log("=== Backfill content_fingerprint ===");
console.log(
`Resuming from cursor id=${state.cursorId} (${state.totalDone} already done)`
`Resuming from cursor created_at=${state.cursorCreatedAt} (${state.totalDone} already done)`
);
console.log(`Batch size: ${BATCH_SIZE}`);
console.log();

while (true) {
state.batches++;
process.stdout.write(
`Batch ${state.batches}: fetching from id>${state.cursorId}… `
`Batch ${state.batches}: fetching from created_at>${state.cursorCreatedAt}… `
);

let rows;
try {
rows = await fetchBatch(state.cursorId, BATCH_SIZE);
rows = await fetchBatch(state.cursorCreatedAt, BATCH_SIZE);
} catch (err) {
console.error("\n Fetch error:", err.message, "— retrying in 5s…");
await new Promise((r) => setTimeout(r, 5000));
Expand All @@ -221,8 +221,8 @@ async function main() {
state.totalDuplicates += duplicates;
state.totalErrors += errors;

const maxId = rows[rows.length - 1].id;
state.cursorId = typeof maxId === "number" ? maxId : maxId;
const lastCreatedAt = rows[rows.length - 1].created_at;
state.cursorCreatedAt = lastCreatedAt;
saveState(state);

const dupeStr =
Expand All @@ -231,7 +231,7 @@ async function main() {
console.log(
` → ${done} patched${dupeStr}${errStr}. ` +
`Total: ${state.totalDone} patched, ${state.totalDuplicates} duplicates, ${state.totalErrors} errors. ` +
`Cursor: ${state.cursorId}`
`Cursor: ${state.cursorCreatedAt}`
);

await new Promise((r) => setTimeout(r, 150));
Expand Down
22 changes: 11 additions & 11 deletions recipes/fingerprint-dedup-backfill/delete-duplicates.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -93,14 +93,14 @@ function buildFingerprint(text) {

// ── REST helpers ────────────────────────────────────────────────────────────

async function fetchBatch(cursorId, batchSize) {
async function fetchBatch(cursorCreatedAt, batchSize) {
const url =
`${REST_BASE}/thoughts` +
`?content_fingerprint=is.null` +
`&id=gt.${cursorId}` +
`&select=id,content` +
`&created_at=gt.${encodeURIComponent(cursorCreatedAt)}` +
`&select=id,content,created_at` +
`&limit=${batchSize}` +
`&order=id.asc`;
`&order=created_at.asc`;
const res = await fetch(url, { headers: HEADERS });
if (!res.ok) {
const body = await res.text().catch(() => "");
Expand Down Expand Up @@ -173,7 +173,7 @@ function loadState() {
return JSON.parse(fs.readFileSync(STATE_FILE, "utf8"));
} catch {
return {
cursorId: 0,
cursorCreatedAt: "1970-01-01T00:00:00Z",
totalDeleted: 0,
totalPatched: 0,
totalWouldDelete: 0,
Expand Down Expand Up @@ -205,19 +205,19 @@ async function main() {
}

console.log(
`Resuming from cursor id=${state.cursorId} (deleted: ${state.totalDeleted}, patched: ${state.totalPatched})`
`Resuming from cursor created_at=${state.cursorCreatedAt} (deleted: ${state.totalDeleted}, patched: ${state.totalPatched})`
);
console.log(`Batch size: ${BATCH_SIZE}\n`);

while (true) {
state.batches++;
process.stdout.write(
`Batch ${state.batches}: fetching from id>${state.cursorId}… `
`Batch ${state.batches}: fetching from created_at>${state.cursorCreatedAt}… `
);

let rows;
try {
rows = await fetchBatch(state.cursorId, BATCH_SIZE);
rows = await fetchBatch(state.cursorCreatedAt, BATCH_SIZE);
} catch (err) {
console.error("\n Fetch error:", err.message, "— retrying in 5s…");
await new Promise((r) => setTimeout(r, 5000));
Expand Down Expand Up @@ -303,14 +303,14 @@ async function main() {
}

// Advance cursor
const maxId = rows[rows.length - 1].id;
state.cursorId = maxId;
const lastCreatedAt = rows[rows.length - 1].created_at;
state.cursorCreatedAt = lastCreatedAt;
saveState(state);

console.log(
` Totals: deleted=${state.totalDeleted}, patched=${state.totalPatched}, ` +
`would-delete=${state.totalWouldDelete}, errors=${state.totalErrors}. ` +
`Cursor: ${state.cursorId}`
`Cursor: ${state.cursorCreatedAt}`
);

await new Promise((r) => setTimeout(r, 200));
Expand Down
2 changes: 1 addition & 1 deletion recipes/fingerprint-dedup-backfill/metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,5 @@
"difficulty": "beginner",
"estimated_time": "15 minutes",
"created": "2026-03-22",
"updated": "2026-03-22"
"updated": "2026-04-03"
}
7 changes: 5 additions & 2 deletions server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -378,8 +378,11 @@ app.options("*", (c) => {
});

app.all("*", async (c) => {
// Accept access key via header OR URL query parameter
const provided = c.req.header("x-brain-key") || new URL(c.req.url).searchParams.get("key");
// Accept access key via Authorization: Bearer header, x-brain-key header, or URL query parameter
const authHeader = c.req.header("Authorization");
const provided = (authHeader?.startsWith("Bearer ") && authHeader.split(" ")[1])
|| 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);
}
Expand Down
Loading