DEV Community

Jonah Reed
Jonah Reed

Posted on

I Built an AI Agent That Thinks in Notion (And Can Give His Brain a Makeover)

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 ScheduledFor are 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:

  1. Boot + daemon init - Backend starts, Notion brain is connected, pollers start.

  2. Decision gate flow - A sensitive action triggers a decision in Notion as pending_approval. Approved directly in Notion. Bob continues only after approval.

  3. Task queue flow - A pending task is added to Tasks. Poller picks it up, transitions pending -> in_progress -> completed, and writes result/timestamps.

  4. Mind customization - Bob applies a preset to his own workspace page. Icon/cover/blocks update in Notion.

Show us the code

GitHub logo 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.

v0.1 License TypeScript Open Source


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
Enter fullscreen mode Exit fullscreen mode
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;
  }
}
Enter fullscreen mode Exit fullscreen mode

Decisions/tasks/mind now use MCP tools like:

  • API-post-page
  • API-patch-page
  • API-query-data-source
  • API-patch-block-children
  • API-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 } }] },
      }
});
Enter fullscreen mode Exit fullscreen mode

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' }],
});
Enter fullscreen mode Exit fullscreen mode

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 });
    }
}
Enter fullscreen mode Exit fullscreen mode

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.' } }] },
    },
  ],
});
Enter fullscreen mode Exit fullscreen mode

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_progress tasks are reset to pending
  • orphaned pending_approval decisions 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:

  • /config database (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!

Check out OpenFiend

Top comments (0)