This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Ultimate Brain MCP is a Python MCP (Model Context Protocol) server that exposes Thomas Frank's Ultimate Brain Notion system as 31 tools for AI assistants. Built on Anthropic's FastMCP SDK with async httpx for Notion API calls.
# Install dependencies
uv sync
# Run the MCP server
uv run ultimate-brain-mcp
# Run MCP dev inspector (interactive tool debugging)
uv run mcp dev src/ultimate_brain_mcp/server.py
# Run all tests (requires .env with valid Notion credentials)
uv run pytest tests/
# Run a single test file
uv run pytest tests/test_formatters.py
# Run a single test by name
uv run pytest tests/test_tools.py -k test_search_tasks
# Build for distribution
uv build
# Release a new version (PR-based flow)
# Bumps version + rolls CHANGELOG + updates uv.lock on a release/vX.Y.Z branch,
# opens an auto-merge PR, waits for it to merge, then tags to trigger publish.
./scripts/release.sh <major|minor|patch> "<commit message>"
./scripts/release.sh patch "Fix bug" --dry-run # branch + commit locally, no push/PR/tag
./scripts/release.sh patch "Fix bug" --no-wait # open the PR but don't block; tag manually after mergeReleases go through the same branch protection + CI gate as any change (the
release commit must pass CI before it merges to main). The v* tag pushed
after merge triggers publish.yml (PyPI publish via OIDC + skill-archive asset).
Requires gh authenticated and repo auto-merge enabled.
Requires .env with Notion credentials. Run uv run python setup_dev.py to auto-discover Notion data source IDs and generate the .env file. See .env.example for required variables.
Six required env vars: NOTION_INTEGRATION_SECRET, UB_TASKS_DS_ID, UB_PROJECTS_DS_ID, UB_NOTES_DS_ID, UB_TAGS_DS_ID, UB_GOALS_DS_ID. Optional: UB_TIMEZONE (IANA name, validated at load) and the secondary database IDs in config.py:SECONDARY_DB_ENV_MAP (UB_WORK_SESSIONS_DS_ID, UB_MILESTONES_DS_ID, UB_PEOPLE_DS_ID, UB_BOOKS_DS_ID, UB_READING_LOG_DS_ID, UB_GENRES_DS_ID, UB_RECIPES_DS_ID, UB_MEAL_PLANNER_DS_ID). The actual env var names live in config.py:UBConfig.from_env.
Source layout: src/ultimate_brain_mcp/ with four modules:
-
server.py— All 31 MCP tool definitions using@mcp.tool()decorators. Tools are grouped: Tasks (6), Projects (4), Notes (4), Tags (3), Goals (4), Cross-cutting (4:daily_summary,archive_item,set_page_content,patch_page_content), Workflow consolidators (2:daily_review_snapshot,bulk_update_tasks), Generic (4:query_database,get_page,get_page_content,update_page). Page-body tools use Notion's server-side Markdown endpoints:get_page_content/get_note_contentread viaget_page_markdown(falls back to the local block→text converter if the 2026-03-11 API version is unavailable),set_page_contentreplace mode writes viareplace_page_markdown(falls back to delete-then-append blocks), andpatch_page_contentapplies targeted find-and-replace edits one at a time viaupdate_page_markdown(no whole-page rewrite; reportsedits_applied/unmatched/failed_at_index, no block fallback). Markdown availability is detected lazily and cached onAppContext.markdown_supported: once a markdown call succeeds, later errors surface instead of silently degrading to the block path (a 400 then means a real content error, not "endpoint missing"); the fallback only triggers on the first version-unavailable error. The 4 create tools (create_task,create_note,create_project,create_goal) accept an optionalcontentparameter for page body content.create_taskandupdate_taskalso accept optionalparent_task_id(Parent Task relation),tag_ids(Tag relation), andlocation(auto-detected from the live Tasks schema).format_tasksurfaceslocationon every task read (search/get/snapshot/bulk results) when the Tasks DB has a Location property — callers pass the discovered property name fromtasks_schema.bulk_update_tasksapplies up to N task patches concurrently with per-row results, never raising on a single failure. Each tool usesToolAnnotationsto declare read-only vs destructive. Property builder helpers (_prop_title,_prop_select,_build_location_payload, etc.) construct Notion API property payloads._coerce_property()auto-converts Python types to Notion property format for the genericupdate_pagetool. -
notion_client.py— Async httpx wrapper around Notion API v2025-09-03. Uses the data_sources query endpoint (not legacy database queries). Core methods:query_all()(paginated),create_page()(supportschildrenfor inline body content),get_page(),update_page(),get_blocks(),append_blocks(),delete_block(). Markdown body methods (get_page_markdown(),replace_page_markdown(),update_page_markdown()) call the/pages/{id}/markdownendpoints, which require API version2026-03-11;_request()accepts a per-callnotion_versionoverride so these endpoints pin the newer version while the rest of the surface stays on 2025-09-03. A token-spaced rate limiter (~3 req/s) plus retry-with-backoff on 429/5xx wrap every request. RaisesNotionAPIErrorwith status-specific hints. -
formatters.py— Transforms raw Notion JSON into agent-friendly dicts. Per-database formatters (format_task,format_project, etc.) plusformat_generic_pagefor secondary databases. Property extractors (_title,_select,_status,_relation, etc.) handle Notion's nested property format. Formatters add a_truncated_relationswarning field (via_annotate_truncation) when a relation exceeds Notion's 25-item inline cap (has_more) — the full list would require the not-yet-implemented paginated page-property endpoint.blocks_to_text()converts page block content to readable text (used on the markdown fallback/append paths).text_to_blocks()does the inverse — parses markdown-like text into Notion block dicts. -
config.py—UBConfigdataclass loaded from env vars. Defines valid statuses, priorities, tag types, and note types as constants. Maps secondary database env var names. Carries the workspacetimezone(validated viazoneinfoat load time, set viaUB_TIMEZONE).extract_property_metadata()introspects Notion data source schemas for select/multi_select/status options — used by lifespan-time Tasks Location + Labels discovery.
Lifecycle: __init__.py:main() validates env vars, then server.py uses an async lifespan context to create/close the NotionClient (stored in mcp.ctx).
Tests require a live Notion workspace with valid credentials in .env. Tests skip gracefully if env vars are missing.
test_tools.py— Spins up the MCP server via stdio client, verifies tool registration, and executes tools against the live API.test_formatters.py— Tests Notion JSON → formatted dict transformations using live data.test_block_builders.py— Unit tests fortext_to_blocks()parser (no Notion credentials needed).test_content.py— Live API integration tests for page body content (create with content, set/append/clear content, get_page_content).conftest.py— Seed fixtures create items prefixed with[TEST]and archive them on teardown.
For exercising tools as a real MCP client (not just pytest), register this working tree as a local dev server and drive it from a headless Claude Code session in a detached tmux window:
# Register the working-tree build (project scope → .mcp.json, which is gitignored).
# `uv run --env-file` loads .env at launch — the server itself does not read .env
# (only the test suite does), so the --env-file flag is required.
claude mcp add ultimate-brain-dev --scope project -- \
uv run --env-file "$PWD/.env" --directory "$PWD" ultimate-brain-mcp
# Run a test prompt headless in a detached tmux session, scoped to only the dev
# tools (no permission bypass). Redirect to a file and append a completion marker.
tmux new-session -d -s ub-test -c "$PWD"
tmux send-keys -t ub-test 'claude -p "$(cat /tmp/ub_test_prompt.txt)" \
--allowedTools "mcp__ultimate-brain-dev__search_tasks mcp__ultimate-brain-dev__update_task ..." \
> /tmp/ub_test_out.txt 2>&1; echo "===EXIT=$?===" >> /tmp/ub_test_out.txt' Enter
# Watch for completion, then read the transcript:
until grep -q '===EXIT=' /tmp/ub_test_out.txt; do sleep 2; doneNotes:
- Prefer
--allowedToolsover--dangerously-skip-permissions— whitelist the specificmcp__ultimate-brain-dev__*tools the test needs. The classifier blocks skip-permissions for spawned agents. - Tools surface as
mcp__ultimate-brain-dev__<tool>in the spawned session. - Have the test prompt create only
[TEST]-prefixed items and clean up afterward. Notearchive_itemrequires the Tasks DB to have anArchivedproperty; where it's absent, verify/clean up via the Notion API directly (a trashed page hasin_trash: trueand is excluded fromsearch_*results but still resolves viaget_page). - Tear down with
tmux kill-session -t ub-testandclaude mcp remove ultimate-brain-dev -s project.
- Add tool function in
server.pywith@mcp.tool()andToolAnnotations - Add formatter in
formatters.pyif the tool returns database items - Add constants to
config.pyif new statuses/types are needed - Add secondary database env vars to
SECONDARY_DB_ENV_MAPinconfig.py— generic tools auto-discover them
Tag with v* (e.g., git tag v0.2.0) and push. GitHub Actions publishes to PyPI via trusted publisher (OIDC).