Reflective — AI journaling companion built with Notion MCP and Claude

This is a submission for the Notion MCP Challenge

What I Built

Reflective is a Chrome extension + Node.js backend that adds an AI journaling companion to your browser sidebar while you write in Notion.

Most journaling tools are write-only. You pour thoughts in, they sit there. Reflective makes your Notion journal a two-way conversation — without leaving the page.

How it works:

You write in Notion
       ↓
Click "Analyze this entry" in the sidebar
       ↓
Claude reads your entry + your journal history from Notion
       ↓
Opens a conversation grounded in what you actually wrote
       ↓
Click "Mark session complete"
       ↓
Mood score, tags, themes, and AI summary written back to Notion

Key behaviors:

  • Sits in the Chrome side panel alongside Notion — no new tabs
  • Context-aware: on a database view it loads your last 50 entries; on a single entry it reads just that page
  • Temporal awareness: if you’re reading an old entry, Claude knows — it frames responses as looking back, not as if it’s today
  • One conversation across the whole session — navigating to a new entry appends to the chat with a — now reading: [title] — divider
  • Analysis is always user-initiated. The extension makes zero API calls on navigation.
  • Mood sparkline shows your last 14 days, pulled live from a Notion database

One-click workspace setup. On first launch, it creates a Journal Entries database, Mood Log, Weekly Summaries database, a seeded starter entry, and a dashboard page — all via the Notion API. You paste your integration token, it does the rest in ~10 seconds.

Video Demo

Show Us the Code

Repo: https://github.com/neicore/notion-reflective

Stack:

  • Chrome Extension (Manifest V3, React + TypeScript, Vite, Tailwind)
  • Node.js + Express backend (TypeScript)
  • Anthropic Claude (claude-sonnet-4-5)
  • Notion API (@notionhq/client v2)
  • Server-Sent Events for streaming progress to the extension
  • pnpm workspaces

How I Used Notion MCP

Notion is where users read and write their data — and it’s also the entire data model. Every piece of state lives there, accessed via @notionhq/client.

Reading: building context for Claude

When you click “Analyze this entry”, the backend fetches in parallel:

const [rawHistory, recentMoods] = await Promise.all([
  fetchJournalEntries(journalDbId, 50, notionToken),  // last 50 entries
  queryMoodHistory(moodLogDbId, 14, notionToken),      // 14-day sparkline
])

Then reads the current page blocks directly:

const [pageObj, blocks] = await Promise.all([
  notion.pages.retrieve({ page_id: pageId }),
  notion.blocks.children.list({ block_id: pageId, page_size: 100 }),
])

The block content becomes the raw text fed to Claude alongside journal history. History is split into “before this date” and “after this date” relative to the entry you’re viewing — that’s what gives Claude correct temporal framing.

Writing: analysis flows back into Notion

When you mark a session complete:

await notion.pages.update({
  page_id: pageId,
  properties: {
    'Mood Score':       { number: result.moodScore },
    'Mood Tags':        { multi_select: result.moodTags.map(name => ({ name })) },
    'Themes':           { multi_select: result.themes.map(name => ({ name })) },
    'AI Summary':       { rich_text: [{ text: { content: result.aiSummary } }] },
    'Word Count':       { number: wordCount },
    'Session Complete': { checkbox: true },
  },
})

Your Notion database gets richer over time. On future analyses, entries with an AI Summary are used as-is (fast); entries without one get their blocks fetched (slower, shown in the loading progress bar). First load hydrates your journal — subsequent loads are instant.

The loading experience

Hydrating 50 entries takes a while the first time. The /api/init endpoint streams progress via Server-Sent Events:

data: {"type":"journal_count","total":47}
data: {"type":"hydrating","index":1,"total":47,"title":"March reflections"}
data: {"type":"hydrating","index":2,"total":47,"title":"A hard week"}
...
data: {"type":"done","entryContent":"...","openingMessage":"..."}

The extension consumes this with fetch + ReadableStream (more reliable than EventSource in extension contexts), updating the UI: Reading your journal — 3/47 — A hard week.

What Notion unlocks that a plain database wouldn’t

Notion’s multi-select properties for Mood Tags and Themes mean once entries are analyzed, you can filter, sort, and group your entire journal by mood or theme natively in Notion — no custom query interface needed. The AI populates the properties; Notion’s built-in views do the rest.

The mood sparkline pulls from a separate Mood Log database that tracks daily mood independently from journal entries. You can log mood on days you don’t write, and the chart reflects it. Two related databases — the kind of structure that’s natural in Notion and would need real schema design anywhere else.

That’s the part I keep coming back to: the AI populates the properties, Notion’s built-in views do the rest. No custom query interface, no separate dashboard to maintain. Your journal just gets smarter over time.