DEV Community

Sailalith Sarupuri
Sailalith Sarupuri

Posted on

Commit Code, Update Docs: Building an AI Pipeline With Notion MCP

Notion MCP Challenge Submission 🧠

This is a submission for the Notion MCP Challenge

What I Built

Codebase Cortex is a multi-agent AI system that automatically keeps engineering documentation in sync with code. You commit code — your Notion docs update themselves.

Why not just use Claude Code / Cursor / Copilot for docs?

Coding AI tools are great at writing documentation — when you prompt them. The problem is you have to keep prompting. Every commit, you need to remember to ask, check if the right pages were updated, and verify nothing was overwritten. It's still a manual process — just with better writing.

Cortex is different because it's autonomous:

Coding AI Tools Codebase Cortex
Trigger You prompt it manually Runs automatically on git commit
Scope Updates what you point it to Finds related docs via semantic search across the entire codebase
Consistency Different output each time Deterministic section-level merge — unchanged sections stay identical
Memory Forgets between sessions FAISS index + page cache persist across runs
Multi-page You manage which pages to update Agents discover which pages need updating
Tracking No audit trail Sprint reports, task creation, content hashes
Workflow Interactive (needs your attention) Background process (runs after commit, logs results)

Think of it like the difference between manually running a linter versus having it in your CI pipeline. Both do the same job — only one actually happens consistently.

The Agents

The system uses five specialized LangGraph agents that work in sequence:

  1. CodeAnalyzer — Reads git diffs and produces a structured analysis of what changed
  2. SemanticFinder — Searches a FAISS vector index to find semantically related code across the entire codebase
  3. DocWriter — Fetches current Notion pages via MCP, generates section-level updates, and merges them deterministically
  4. TaskCreator — Identifies undocumented areas and creates task pages in Notion
  5. SprintReporter — Generates weekly sprint summaries and appends them to a Sprint Log page

Key Features

  • Automatic doc sync — Post-commit git hook triggers the pipeline with zero human effort
  • Section-level updates — Only changed sections are rewritten; unchanged sections are preserved exactly as-is
  • Semantic search — FAISS embeddings (all-MiniLM-L6-v2, 384-dim) find related code, not just filename matches
  • Natural language prompts — cortex prompt "Make the API docs more detailed" for directed updates
  • Multi-page intelligence — Agents understand relationships across all doc pages
  • Confirmation flow — Shows planned changes and asks for approval before writing
  • Pluggable LLMs — Works with Google Gemini, Anthropic Claude, or any OpenRouter model
  • Published on PyPI — pip install codebase-cortex

The Pipeline

Git Commit
    ↓
CodeAnalyzer (parse diff, LLM analysis)
    ↓
SemanticFinder (embed analysis, FAISS search for top-10 related chunks)
    ↓
DocWriter (fetch Notion pages via MCP → LLM generates section_updates → deterministic merge → write via MCP)
    ↓
TaskCreator (LLM identifies gaps → create task pages in Notion via MCP)
    ↓
SprintReporter (LLM generates summary → append to Sprint Log via MCP)
    ↓
Notion Workspace (updated docs, new tasks, sprint report)
Enter fullscreen mode Exit fullscreen mode

Section-Level Merging (the core innovation)

Instead of rewriting entire pages (which causes "LLM drift"), DocWriter:

  1. Fetches the existing page from Notion via notion-fetch
  2. Parses it into sections split by markdown headings
  3. Asks the LLM to return ONLY the sections that changed
  4. Merges the changes into the existing structure deterministically
  5. Writes the full merged content back via notion-update-page

This means a page with 10 sections where 1 section needs updating gets exactly 1 section changed. The other 9 sections are byte-for-byte identical to before.

Video Demo

Install and try it:

pip install codebase-cortex

cd your-project
cortex init        # Interactive setup (LLM, Notion OAuth, starter pages)
cortex run --once  # Run the full pipeline
Enter fullscreen mode Exit fullscreen mode

Project structure

codebase-cortex/
├── src/codebase_cortex/
│   ├── cli.py              # Click CLI (init, run, status, prompt, scan, embed)
│   ├── config.py            # Settings, get_llm() factory
│   ├── state.py             # CortexState TypedDict (shared pipeline state)
│   ├── graph.py             # LangGraph StateGraph with conditional routing
│   ├── mcp_client.py        # Notion MCP connection (Streamable HTTP + OAuth)
│   ├── agents/
│   │   ├── base.py          # BaseAgent ABC with LLM invocation
│   │   ├── code_analyzer.py # Git diff / full codebase analysis
│   │   ├── semantic_finder.py # FAISS similarity search
│   │   ├── doc_writer.py    # Section-level Notion page updates
│   │   ├── task_creator.py  # Task page creation
│   │   └── sprint_reporter.py # Sprint summary generation
│   ├── auth/                # OAuth 2.0 + PKCE for Notion
│   ├── embeddings/          # sentence-transformers, FAISS, HDBSCAN
│   ├── git/                 # Diff parsing, GitHub client
│   ├── notion/              # Page bootstrap and cache
│   └── utils/               # Rate limiter, section parser, JSON parsing
├── tests/                   # 74 tests
├── docs/                    # Full documentation with mermaid diagrams
└── pyproject.toml
Enter fullscreen mode Exit fullscreen mode

Key technical decisions

Decision Why
LangGraph StateGraph Conditional routing — skip agents when no changes detected, skip sprint report when nothing to report
FAISS IndexFlatL2 Exact nearest-neighbor search, no training required, sub-millisecond for medium codebases
Section-level merging Deterministic merge prevents LLM drift; unchanged sections preserved exactly
sentence-transformers all-MiniLM-L6-v2 is fast, CPU-friendly, 384-dim — good balance of quality and speed
HDBSCAN Auto-determines cluster count, handles noise — better than k-means for code topology
Dual rate limiter Async token bucket (180 req/min general + 30 req/min search) matches Notion's limits exactly
OAuth 2.0 + PKCE Browser-based auth with dynamic client registration — zero manual token setup

How I Used Notion MCP

Codebase Cortex uses Notion MCP as its primary interface to Notion — every read and write goes through MCP. The system connects via Streamable HTTP to https://mcp.notion.com/mcp with OAuth bearer token authentication.

MCP tools used and how

notion-fetch — Read page content

Used by DocWriter to fetch current page content before generating updates. Also used during init to detect page renames and sync titles back to the local cache.

result = await session.call_tool(
    "notion-fetch",
    arguments={"id": page_id},
)
content = strip_notion_metadata(result.content[0].text)
Enter fullscreen mode Exit fullscreen mode

notion-update-page — Write page updates

Used by DocWriter to write section-merged content, and by SprintReporter to append sprint summaries.

# Replace entire page content (after local section merge)
await session.call_tool(
    "notion-update-page",
    arguments={
        "page_id": page_id,
        "command": "replace_content",
        "new_str": merged_content,
    },
)

# Append to existing page (sprint reports)
await session.call_tool(
    "notion-update-page",
    arguments={
        "page_id": page_id,
        "command": "insert_content_after",
        "new_str": sprint_report,
    },
)
Enter fullscreen mode Exit fullscreen mode

notion-create-pages — Create new pages

Used by DocWriter for new documentation pages, TaskCreator for task pages, and the bootstrap process for starter pages.

result = await session.call_tool(
    "notion-create-pages",
    arguments={
        "pages": [{
            "properties": {"title": "API Reference"},
            "content": "# API Reference\n\n..."
        }],
        "parent": {"page_id": parent_id},
    },
)
Enter fullscreen mode Exit fullscreen mode

notion-search — Search workspace

Used by the cortex scan command to discover existing pages and by the page discovery process to find new child pages.

What Notion MCP unlocks

  1. Zero-setup Notion access — OAuth 2.0 with PKCE means users just click "Allow" in their browser. No manual integration creation, no API token copying, no Notion developer portal.

  2. AI-native page interaction — MCP returns content in markdown format, which is exactly what LLMs work best with. No HTML parsing, no block-level API complexity.

  3. Section-level updates — The replace_content command lets us write full merged content after doing section-level merging locally. This gives us deterministic control over what changes.

  4. Stateless connection — Each pipeline run opens a fresh MCP session with auto-refreshed tokens. No persistent connection to manage, no websocket heartbeats.

  5. Rate-limit friendly — The dual token bucket (180/min general, 30/min search) ensures we never hit Notion's limits, even when updating multiple pages in a single pipeline run.

Handling Notion MCP quirks

One non-obvious challenge: the notion-fetch tool returns page content with literal \n (backslash + n) instead of real newline characters. This broke section parsing until we added an unescape step:

def _unescape_notion_text(text: str) -> str:
    """Convert literal \\n and \\t from Notion MCP to real characters."""
    result = []
    i = 0
    while i < len(text):
        if text[i] == '\\' and i + 1 < len(text):
            next_char = text[i + 1]
            if next_char == 'n':
                result.append('\n')
                i += 2
                continue
            elif next_char == 't':
                result.append('\t')
                i += 2
                continue
        result.append(text[i])
        i += 1
    return ''.join(result)
Enter fullscreen mode Exit fullscreen mode

This is applied before any content parsing, making the MCP encoding transparent to the rest of the system.

MCP integration architecture

cortex run --once
    ↓
[Agents run, produce doc_updates]
    ↓
DocWriter opens MCP session (OAuth token auto-refresh)
    ↓
For each page to update:
    → rate_limiter.acquire()
    → notion-fetch (read current content)
    → _unescape_notion_text() + strip_notion_metadata()
    → parse_sections() + LLM generates section_updates
    → merge_sections() (deterministic)
    → rate_limiter.acquire()
    → notion-update-page (replace_content)
    → page_cache.upsert() (update content hash)
    ↓
MCP session closes
Enter fullscreen mode Exit fullscreen mode

Every MCP call goes through an async rate limiter and a LoggingSession wrapper that logs tool names, arguments, and response previews when verbose mode is enabled.

Top comments (0)