This is a submission for the Notion MCP Challenge
(Fair warning: I found out about this challenge a week late. If I'm past the deadline, blame my feed algorithm ty ily <3)
What I Built
Hey y'all! Quick context: I believe you've met OpenFiend in my previous post, so this one is all about the Notion MCP build.
The idea: what if Notion wasn't where an AI agent just stores things, but where it actually... thinks? Decisions, tasks, memory, workspace customization - all living in Notion, all perfectly readable AND editable?
This is an experiment in making AI agents transparent by design. If Bob's brain is a Notion workspace, you can open it up, read his reasoning, override his decisions, and shape how he operates with basically no code required (though you'll probably feel like you're writing some!).
What Bob can do today:
Decision gates (implemented + tested) - Before Bob runs a risky action, he writes a proposal to Notion Decisions as
pending_approval. Approving directly in Notion (or in the UI - the choice is still yours) unblocks Bob.Async task queue (implemented + tested) - Drop a task in Tasks with
pending; Bob picks highest-priority work first, executes, writes result, and updates status.Scheduled tasks (implemented, lightly tested) - Tasks with future
ScheduledForare skipped until due time. Bob is quite punctual when circumstances allow.Mind customization (implemented + tested) - Bob can restyle his own Notion root page. He can preset themes, icons, covers, and block layouts (just so the workspace feels personal and less generic).
Persistent memory & will (implemented, not deeply tested in demo) - Bob can read and write long-term memory and "will" statements to Notion, injecting them as runtime context. Deeper integration into the live session loop is coming in later updates.
Autopsy reports (implemented, not demo-tested) - Failure report write/read module exists.
Threat logging (implemented, not demo-tested) - Bob can log and retrieve security events from Notion. The module works but isn't wired into active detection paths yet.
Shadow logs (implemented, not demo-tested) - Shadow observation read/write/enable exists but was not included in the final demo run.
The tech stack:
- Frontend: React + Vite + Tailwind CSS (3-panel layout: chat, history, audit trail)
- Backend: Node.js + Express + WebSockets
- Database: SQLite (via Drizzle ORM) for local persistence
- AI: Vercel AI SDK with Anthropic Claude
- Notion: Root page + structured databases for agent state
-
MCP:
@notionhq/notion-mcp-server+@modelcontextprotocol/sdk
Video Demo
What the demo shows:
Boot + daemon init - Backend starts, Notion brain is connected, pollers start.
Decision gate flow - A sensitive action triggers a decision in Notion as
pending_approval. Approved directly in Notion. Bob continues only after approval.Task queue flow - A pending task is added to Tasks. Poller picks it up, transitions
pending->in_progress->completed, and writes result/timestamps.Mind customization - Bob applies a preset to his own workspace page. Icon/cover/blocks update in Notion.
Show us the code
jreed18
/
openfiend
Bob. A fiend on your side. Local-first AI agent that shows every move and asks before making them.
O P E N F I E N D
NO BLACK BOXES. NO TRUST REQUIRED.
Security-first AI agent platform.
Every action visible. Every permission explicit. Everything auditable.
> WHAT IS THIS
OpenFiend is an AI agent platform where transparency isn't a feature — it's the architecture.
The first agent — Bob — is a paranoid, audit-log-obsessed assistant powered by Claude. You talk to Bob through a real-time WebSocket chat interface. Everything he does is logged, visible, and controllable.
This is v0.1. It's early and messy, but we're (I'm) making it work!
⚠️ Not production-ready: OpenFiend is an early-stage project and has not been tested thoroughly enough for production use yet.
> QUICK START
git clone https://github.com/jreed18/openfiend.git
cd openfiend
cp .env.example .env.local # add your API keys (see .env.example)
pnpm install
pnpm dev
| Service | URL |
|---|---|
| Frontend | localhost:5173 |
| Backend | localhost:3737 |
| WebSocket | ws://localhost:3737/ws |
Requires Node.js 22+ and pnpm 9+
> ARCHITECTURE
┌──────────────────────────────────────────────────────────────┐
│…The Notion integration lives in packages/backend/src/tools/notion/. Key files:
| File | What it does |
|---|---|
mcpClient.ts |
MCP connection + API-* tool calls + data source lookup cache |
client.ts |
Original Notion SDK client (pre-MCP) |
setup.ts |
First-run workspace/database creation + ID persistence |
poller.ts |
Background polling for decisions + tasks |
sections/decisions.ts |
Ethical review board (write/read/approve) via MCP |
sections/tasks.ts |
Async task queue + scheduling via MCP |
sections/mind.ts |
Root page customization via MCP |
sections/memory.ts |
Long-term memory + will statements |
sections/autopsies.ts |
Post-mortem failure reports |
sections/threats.ts |
Security event feed |
sections/shadow.ts |
Shadow mode observations |
How I Used Notion MCP
Notion isn't meant to be just a storage layer for OpenFiend. I am working on further developing it as the identity and control layer.
The architecture
Bob uses an MCP adapter (mcpClient.ts) to call Notion tools exposed by @notionhq/notion-mcp-server.
// packages/backend/src/tools/notion/mcpClient.ts
export async function callNotionTool<T = any>(name: string, args: Record<string, any>): Promise<T> {
const client = await getNotionMcpClient();
const response: any = await client.callTool({ name, arguments: args });
const first = response?.content?.[0];
const text = first && 'text' in first ? first.text : '';
if (!text) return {} as T;
try {
return JSON.parse(text) as T;
} catch {
return ({ raw: text } as unknown) as T;
}
}
Decisions/tasks/mind now use MCP tools like:
API-post-pageAPI-patch-pageAPI-query-data-sourceAPI-patch-block-childrenAPI-retrieve-a-database
Why MCP specifically
MCP gave me a standardized interface for Notion operations and made it easier to keep Notion access in one adapter layer (plus, the challenge kinda required it).
Decision gates via MCP
Before Bob takes a risky action, he writes a structured proposal to Notion as pending_approval:
// packages/backend/src/tools/notion/sections/decisions.ts
const response = await callNotionTool<any>('API-post-page', {
parent: {
type: 'database_id',
database_id: decisionsDbId,
},
properties: {
Action: { title: [{ text: { content: data.action } }] },
Reasoning: { rich_text: [{ text: { content: data.reasoning } }] },
Risks: { rich_text: [{ text: { content: data.risks } }] },
Risk: { select: { name: data.riskLevel } },
Status: { select: { name: data.status } },
ConversationId: { rich_text: [{ text: { content: data.conversationId } }] },
Annotation: { rich_text: [{ text: { content: data.annotation } }] },
Timestamp: { date: { start: data.timeStart, end: data.timeEnd } },
Tool: { rich_text: [{ text: { content: data.tool } }] },
}
});
The user approves or rejects directly in Notion. The poller (below) detects the status change and unblocks Bob.
Task queue via MCP
Bob queries pending tasks sorted by priority:
// packages/backend/src/tools/notion/sections/tasks.ts
const dataSourceId = await getDataSourceId(tasksDbId);
const notionResult = await callNotionTool<any>('API-query-data-source', {
data_source_id: dataSourceId,
filter: {
property: 'Status',
select: { equals: 'pending' },
},
sorts: [{ property: 'Priority', direction: 'descending' }],
});
The polling pattern (and why not webhooks)
OpenFiend runs locally. Polling is simpler and reliable without public webhook infra.
- Decision poller (10s) - Watches decision status changes and resolves pending permission promises.
- Task poller (60s, dynamic) - Picks pending tasks, runs them, writes results back.
// packages/backend/src/tools/notion/poller.ts
const decisions = await readPendingDecisions();
for (const decision of decisions) {
const { pageId, status: currentStatus } = decision;
if (!pageId) continue;
// Check if we've seen this decision before and if its status has changed
const previousStatus = statusCache.get(pageId);
statusCache.set(pageId, currentStatus);
// First time seeing this decision - cache and skip
if (!previousStatus) continue;
if (previousStatus === 'pending_approval'
&& (currentStatus === 'approved' || currentStatus === 'rejected')) {
const eventType = currentStatus === 'approved' ? 'decision_approved' : 'decision_rejected';
console.log(`[Notion Poller] Detected decision ${currentStatus} for pageId ${pageId}`);
resolveDecision(pageId, currentStatus === 'approved' ? PermissionStatus.Approved : PermissionStatus.Rejected);
broadcastToClients({ type: eventType, decision });
}
}
Mind customization via MCP
// packages/backend/src/tools/notion/sections/mind.ts - apply_preset action
await callNotionTool('API-patch-page', {
page_id: rootPageId,
icon: { type: 'external', external: { url: 'https://www.notion.so/icons/brain_gray.svg' } },
cover: { type: 'external', external: { url: 'https://images.unsplash.com/photo-1518770660439-4636190af475' } },
});
await callNotionTool('API-patch-block-children', {
block_id: rootPageId,
children: [
{
object: 'block', type: 'heading_2',
heading_2: { rich_text: [{ type: 'text', text: { content: 'Mind' } }] },
},
{
object: 'block', type: 'paragraph',
paragraph: { rich_text: [{ type: 'text', text: { content: 'Focused layout. Keep only what matters.' } }] },
},
],
});
What this unlocks
- Human-in-the-loop approvals directly in Notion
- Async Notion task execution without opening chat
- Agent workspace customization as a first-class operation
Crash recovery
On restart:
-
in_progresstasks are reset topending - orphaned
pending_approvaldecisions are auto-rejected
No manual cleanup required. Throughout development I've come to know how annoying it can get...
Upcoming Features
Guided by my internal "Bob's Horcrux" plan (a little code name for this project), next features are:
-
/configdatabase (not implemented yet) - Notion-driven runtime config (model, thresholds, shadow toggle, tools)
Poll config every 60s and apply without restart
/weekly-report(not implemented yet)Sunday Notion report generated from SQLite audit logs
Structured sections: wins, struggles, unusual events, next-week plan
Horcrux-level memory + will loop
Read Will + recent memory on startup and inject into live system context every session
Expand memory writes from simple storage to structured user/profile/project summaries
Support session-linked memory trails for better long-horizon continuity
Deeper wiring for existing sections
Threats/autopsies/shadow are implemented as modules, but need fuller production wiring + dedicated demo coverage
Threats: wire directly into prompt-injection and suspicious-action detection paths
Autopsies: auto-create on critical failures and tool-chain exceptions
Shadow log: first-class review workflow where users can promote safe actions into enabled behavior. Will become rather crucial once the shadow / observer mode gets fully implemented
Notion-native control surface
Make Notion the primary control panel for Bob behavior tuning, not just a log sink
Add "editable policy" pages so non-technical users can safely shape Bob's operating rules
More granular mind customization: let users define their own presets, per-section styling, and layout preferences beyond the built-in themes
Broader MCP adoption
Continue moving remaining Notion sections to MCP transport for full consistency
Toward full Horcrux mode
Goal: if users opt in, Notion becomes Bob’s complete operational identity layer
Decisions, tasks, memory, policies, reports, and configuration all become transparent and editable in one place
If Notion is disconnected, Bob should degrade gracefully while clearly signaling that his "brain state" is unavailable
Last-Minute Reality Check
I initially built this integration with the Notion SDK, then realized late that the challenge specifically required Notion MCP as a core part of the build.
So I did a last-minute refactor to route the core paths (decisions, tasks, and mind customization) through @notionhq/notion-mcp-server + @modelcontextprotocol/sdk.
It wasn't the most comfortable timing, but it pushed the architecture in a better direction: a single MCP adapter layer for Notion operations, with clearer boundaries for future expansion.
I'm building OpenFiend in public because I think AI agents should be built in the open. If you've got thoughts on transparent AI, agent architecture, or just want to tell Bob he's doing great (he needs it), we'd both love to hear from you!
Take care, polar bear!
Top comments (0)