From 15f1935404f31450666a975a57f0b38cf617fd97 Mon Sep 17 00:00:00 2001 From: Sanchay Date: Sun, 19 Apr 2026 13:39:41 +0530 Subject: [PATCH 1/2] Add Notion page operations skill to pm-toolkit Smart router for Notion across two MCP servers with automatic fallback chains, safety verification, and self-learning from failures. Covers read, create, update, section rewrites, table edits, search, and database operations with 12 documented pitfalls and workarounds. Co-Authored-By: Claude Opus 4.6 --- pm-toolkit/README.md | 6 +- pm-toolkit/commands/notion.md | 268 ++++++++++++++++++++++++++++++++++ 2 files changed, 272 insertions(+), 2 deletions(-) create mode 100644 pm-toolkit/commands/notion.md diff --git a/pm-toolkit/README.md b/pm-toolkit/README.md index 9c01cca..5638a5e 100644 --- a/pm-toolkit/README.md +++ b/pm-toolkit/README.md @@ -2,16 +2,18 @@ PM utility skills: resume review, NDA drafting, privacy policy generation, and grammar/flow checking. Essential tools for product managers beyond core product work. -## Skills (4) +## Skills (5) - **draft-nda** — Draft a detailed Non-Disclosure Agreement between two parties. - **grammar-check** — Identify grammar, logical, and flow errors in text and suggest targeted fixes without rewriting the entire text. +- **notion** — Orchestrate Notion page operations with smart routing across two MCP servers, automatic fallback chains, safety verification, and self-learning from failures. - **privacy-policy** — Draft a detailed privacy policy for a product covering data types, jurisdiction, compliance considerations, and clauses needing legal review. - **review-resume** — Comprehensive PM resume review and tailoring against 10 best practices including XYZ+S formula, keyword optimization, job-specific tailoring, and structure. -## Commands (5) +## Commands (6) - `/pm-toolkit:draft-nda` — Draft a Non-Disclosure Agreement between two parties with jurisdiction-appropriate clauses. +- `/pm-toolkit:notion` — Orchestrate Notion page operations — read, create, update, section rewrites, table edits, search, and database ops with fallback chains. - `/pm-toolkit:privacy-policy` — Draft a privacy policy covering data collection, usage, storage, and compliance requirements. - `/pm-toolkit:proofread` — Check grammar, logic, and flow in any text — targeted fixes without rewriting. - `/pm-toolkit:review-resume` — Comprehensive PM resume review against 10 best practices — structure, impact metrics, keywords, and actionable feedback. diff --git a/pm-toolkit/commands/notion.md b/pm-toolkit/commands/notion.md new file mode 100644 index 0000000..5a184bd --- /dev/null +++ b/pm-toolkit/commands/notion.md @@ -0,0 +1,268 @@ +--- +description: Orchestrate Notion page operations — read, create, update, section rewrites, table edits, search, and database ops with automatic fallback chains and safety verification +argument-hint: "[read|create|update|section|table|search|db|learn|diagnose] " +--- + +# /notion — Notion Page Operations + +Smart router for Notion across two MCP servers. Picks the right tool, handles failures, learns from mistakes. + +## Invocation + +``` +/notion read +/notion create [content source] +/notion update <page URL or ID> <what to change> +/notion section <page URL or ID> <heading name> [new content or instructions] +/notion table <page URL or ID> <what to change in the table> +/notion search <query> +/notion db [query|add|update|schema] <database URL or ID> [details] +/notion learn "<finding description>" +/notion diagnose +/notion <page URL or ID> # interactive — asks what to do +/notion # interactive — asks for page and operation +``` + +--- + +## Companion Files + +This skill works with two companion files that should be loaded during write operations: + +- **`notion-operations-guide.md`** — Tool inventory, decision matrices, block estimation, known pitfalls (P1–P12) +- **`notion-learnings.md`** — Living knowledge base of discovered patterns, workarounds, and optimizations + +--- + +## Safety Protocol + +**Every write operation follows this protocol. No exceptions.** + +### Before Writing +1. Read the page with `read_page` → store full content as `$BEFORE` +2. If `read_page` fails, fall back to alternate MCP's `fetch` +3. For destructive operations (`replace_content`, large section rewrites), confirm with the user first + +### After Writing +4. Read the page again → store as `$AFTER` +5. Verify: target content changed as intended +6. Check: no unrelated content missing (compare `$BEFORE` and `$AFTER`) +7. If content was lost: IMMEDIATELY alert the user, display what was lost, offer to restore + +### On Failure — Auto-Learn +8. When a primary tool fails and a fallback succeeds, append the failure pattern to the learnings file + +--- + +## Mode: read + +**Purpose:** Read and display a Notion page's content. + +1. Extract page URL or ID from arguments +2. **Primary:** `read_page` — returns full markdown, no truncation +3. **Fallback:** Alternate MCP `fetch` — may truncate large pages (~54KB); warn user +4. Display the content to the user + +--- + +## Mode: create + +**Purpose:** Create a new Notion page with automatic chunking for large content. + +1. Gather: parent page (URL/ID), title, content (from user, local file, or conversation) +2. **Never push partial content.** Always push the complete document. + +3. **Estimate block count:** + - Count: paragraphs, headings, list items (1 each), table rows (1 each), code blocks (1), dividers (1) + - Tables are deceptive: a 10-column, 20-row table = 21 blocks (header + 20 rows) + +4. **If estimated blocks ≤ 90:** + - `create_page(parent_page_id, title, markdown)` + +5. **If estimated blocks > 90:** + - Split content at a natural heading boundary near block 80-90 + - `create_page(parent_page_id, title, first_chunk)` + - `append_content(new_page_id, next_chunk)` — repeat for each remaining chunk + - Each append chunk should also stay under 90 blocks + +6. **Verify:** Read the created page back. Confirm key sections exist and content is complete. + +--- + +## Mode: update + +**Purpose:** Smart-routed targeted edit. Picks the best tool based on the edit type. + +### Step 1: Read the page (Safety Protocol) + +### Step 2: Classify the edit + +- **Full page rewrite?** → Confirm with user, then use `replace_content` or create a new page +- **Section rewrite?** → Redirect to **section** mode +- **Table edit?** → Redirect to **table** mode +- **Targeted text edit?** → Continue to Step 3 + +### Step 3: Check text uniqueness + +Search `$BEFORE` content for the target text. + +**Case A — Text is globally unique (1 match):** +``` +USE: find_replace(page_id, old_text, new_text) +``` + +**Case B — Text appears multiple times, ALL should change:** +``` +USE: find_replace(page_id, old_text, new_text, replace_all=true) +``` + +**Case C — Text appears multiple times, only SOME should change:** +``` +WARNING: find_replace cannot target specific instances. +Option 1: Add surrounding context to make old_str unique via alternate MCP's update_content +Option 2: Use section mode to rewrite the enclosing section +``` + +**Case D — "No matches found" error:** +``` +CHECK: em-dash (—) vs dash (-), backtick formatting, extra whitespace +RETRY with corrected text +FALLBACK: Use alternate MCP's update_content +``` + +--- + +## Mode: section + +**Purpose:** Rewrite an entire section identified by heading name. + +1. Read the page (Safety Protocol) +2. Identify the exact heading name — must match what `read_page` returns +3. Prepare the complete new section content + +4. **Primary:** `update_section(page_id, heading, new_content)` + +5. **On TypeError ("Cannot read properties of undefined"):** + - **IMMEDIATELY** re-read the page — `update_section` may have partially deleted content + - Compare with `$BEFORE` to identify what was lost + - **Fallback — Heading-Match Trick:** + Find the NEXT heading after the target section. Match it via alternate MCP's `update_content`, prepend your new content + the original heading. + +6. **On "Heading not found" error:** + - Heading text must match EXACTLY (case-insensitive, but full text including parentheticals) + - Some headings may not exist as proper heading blocks (bold text or paragraphs) + +7. Verify (Safety Protocol) + +--- + +## Mode: table + +**Purpose:** Table-specific operations — the hardest case in Notion. + +**Critical:** Notion stores table cells as separate blocks. Markdown `| row |` notation is a read-time representation — it cannot be matched by any find/replace tool. + +### Decision Tree + +**Edit a cell with UNIQUE content:** +``` +USE: find_replace(page_id, "cell text without pipes", "new cell text") +NOTE: This may still fail on table cells — verify after writing +``` + +**Edit a cell with NON-UNIQUE content:** +``` +Must rewrite the entire table. +1. Find the heading above the table +2. Use section mode to rewrite that heading's content (including the full corrected table) +``` + +**Add a new row:** +``` +WARNING: append_content creates a NEW table, not an additional row. +Use section mode to rewrite the section containing the table with the new row included. +``` + +**Add/remove columns or restructure:** +``` +Use section mode to rewrite the entire table section with the new structure +``` + +### Formatting Rules +- **Never use `|` inside cell values** — use `/` instead (e.g., `option_a / option_b`) +- **Em-dash `—` ≠ dash `-`** — copy exact character from read output +- **Block count:** Each table row ≈ 1 block. A 20-row table ≈ 21 blocks + +--- + +## Mode: search + +**Purpose:** Find Notion pages or databases. + +1. **Primary:** `search(query)` — simple text search +2. **For advanced filters:** Alternate MCP search — supports date ranges, created_by, type filters +3. Display results with titles, URLs, and last edited dates + +--- + +## Mode: db + +**Purpose:** Database operations — query, add entries, update entries, manage schema. + +**Query:** `get_database(db_id)` first for schema, then `query_database(db_id, filter, sort)` + +**Add entry:** `add_database_entry(db_id, properties)` — bulk: `add_database_entries(db_id, entries[])` + +**Update entry:** `update_database_entry(entry_id, properties)` + +**Schema changes:** Alternate MCP's `update-data-source` — add, rename, drop columns + +**View management:** Alternate MCP's `create-view` / `update-view` (table, board, calendar, etc.) + +--- + +## Mode: learn + +**Purpose:** Append a new discovery to the living knowledge base. + +1. Read the learnings file +2. Determine category: `pitfall`, `workaround`, `new-tool`, `behavior-change`, `optimization` +3. Append a new entry with date, category, description, workaround, applicable tools +4. Write the updated file + +If the file exceeds 200 lines, suggest consolidating duplicate entries (but never auto-delete). + +--- + +## Mode: diagnose + +**Purpose:** Check which Notion MCPs are connected and what tools are available. + +1. Probe primary MCP — try a search call +2. Probe alternate MCP — try a search call +3. Report status: CONNECTED / DISCONNECTED for each +4. If UUID prefix changed, log the new prefix + +--- + +## Fallback Chain Summary + +| Operation | Primary | Fallback 1 | Fallback 2 | +|-----------|---------|------------|------------| +| Read | `read_page` | Alt `fetch` | — | +| Create | `create_page` + `append` | Alt `create-pages` | — | +| Unique text edit | `find_replace` | Alt `update_content` | Section rewrite | +| Non-unique edit | Alt `update_content` | Section rewrite | Full replace | +| Section rewrite | `update_section` | Heading-match trick | Full replace | +| Table cell (unique) | `find_replace` | Section rewrite | — | +| Table cell (non-unique) | Section rewrite | — | — | +| Search | `search` | Alt `search` | — | +| DB schema | Alt `update-data-source` | — | — | + +## Notes + +- The Heading-Match Trick: match a heading as `old_str`, replace with `[new content]\n### Heading Name` to insert content before it +- `update_section` TypeError is page-specific — triggered by deep nesting, not raw block count. Try it first, fall back on error +- `append_content` on tables creates a new table, not a new row — always rewrite the section instead +- `find_replace` fails on table cells even for unique text — only reliable on paragraphs, headings, callouts, lists +- Always maintain a local page ID index file for multi-page Notion structures From b1835e670615664864afcb56365b5f371efa63ec Mon Sep 17 00:00:00 2001 From: Sanchay <sanchay@eazyapp.tech> Date: Sun, 19 Apr 2026 13:57:03 +0530 Subject: [PATCH 2/2] Add companion files for Notion skill (operations guide + learnings) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The command file referenced two companion files that didn't exist in the repo — making the skill incomplete for anyone cloning it. Adds: - skills/notion/SKILL.md — skill definition with safety protocol - skills/notion/notion-operations-guide.md — P1-P12 pitfalls, tool selection matrix, block estimation, heading-match trick - skills/notion/notion-learnings.md — 19 living knowledge entries from real Notion MCP usage (failures, workarounds, optimizations) Also updates the command to reference companion files via relative path instead of absolute. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --- pm-toolkit/commands/notion.md | 4 +- pm-toolkit/skills/notion/SKILL.md | 54 +++++ pm-toolkit/skills/notion/notion-learnings.md | 188 +++++++++++++++ .../skills/notion/notion-operations-guide.md | 220 ++++++++++++++++++ 4 files changed, 465 insertions(+), 1 deletion(-) create mode 100644 pm-toolkit/skills/notion/SKILL.md create mode 100644 pm-toolkit/skills/notion/notion-learnings.md create mode 100644 pm-toolkit/skills/notion/notion-operations-guide.md diff --git a/pm-toolkit/commands/notion.md b/pm-toolkit/commands/notion.md index 5a184bd..e4c23b6 100644 --- a/pm-toolkit/commands/notion.md +++ b/pm-toolkit/commands/notion.md @@ -27,11 +27,13 @@ Smart router for Notion across two MCP servers. Picks the right tool, handles fa ## Companion Files -This skill works with two companion files that should be loaded during write operations: +This skill works with two companion reference files located in `skills/notion/`: - **`notion-operations-guide.md`** — Tool inventory, decision matrices, block estimation, known pitfalls (P1–P12) - **`notion-learnings.md`** — Living knowledge base of discovered patterns, workarounds, and optimizations +Load these before any write operation. They contain the full tool inventory, pitfall catalog, and accumulated workarounds. + --- ## Safety Protocol diff --git a/pm-toolkit/skills/notion/SKILL.md b/pm-toolkit/skills/notion/SKILL.md new file mode 100644 index 0000000..2a0ae74 --- /dev/null +++ b/pm-toolkit/skills/notion/SKILL.md @@ -0,0 +1,54 @@ +--- +name: notion +description: "Orchestrate Notion page operations — read, create, update, section rewrites, table edits, search, and database ops. Routes across two MCP servers with automatic fallback chains, safety verification, and self-learning. Use whenever working with Notion pages." +--- + +## Notion Page Operations + +Smart router for Notion across two MCP servers. Picks the right tool, handles failures, learns from mistakes. + +### Context + +You are performing Notion page operations for **$ARGUMENTS**. + +Before any write operation, load the companion reference files in this directory: +- `notion-operations-guide.md` — Tool inventory, decision matrices, block estimation, known pitfalls (P1–P12) +- `notion-learnings.md` — Living knowledge base of discovered patterns, workarounds, and optimizations + +### Instructions + +**Safety Protocol — every write follows this. No exceptions.** + +1. Read the page with `read_page` → store full content as `$BEFORE` +2. If `read_page` fails, fall back to alternate MCP's `fetch` +3. For destructive operations (`replace_content`, large section rewrites), confirm with the user first +4. After writing, read the page again → store as `$AFTER` +5. Verify: target content changed as intended, no unrelated content missing +6. If content was lost: IMMEDIATELY alert the user, display what was lost, offer to restore +7. When a primary tool fails and a fallback succeeds, append the failure pattern to the learnings file + +**Tool selection decision tree:** + +- Full page rewrite → `replace_content` (NEVER on pages with children) or create new page +- Section rewrite → `update_section` (falls back to heading-match trick on TypeError) +- Unique text in paragraph/heading/callout → `find_replace` +- Non-unique text → alternate MCP's `update_content` with surrounding context, or section rewrite +- Table cell edit → ALWAYS use section rewrite (find_replace fails silently on table cells) +- Large page create (>90 blocks) → `create_page` + `append_content` in chunks + +**Critical rules:** +- NEVER use `replace_content` on pages with children — it archives all child pages +- `find_replace` only works on rich text blocks — paragraphs, headings, callouts, lists. Never on table cells. +- `append_content` on tables creates a NEW table, not a new row — rewrite the section instead +- Never use `|` inside table cell values — use `/` instead +- Em-dash `—` ≠ dash `-` — copy exact characters from read output +- After `update_section` TypeError, IMMEDIATELY re-read — it partially deletes content +- Maintain a local page ID index file for multi-page structures +- Batch updates: list_pages → restore missing → sample read → batches of 3-4 → verify + +### Output + +After completing the operation: +- Confirm what changed with a brief summary +- If any fallback was triggered, report which tool failed and which succeeded +- Offer relevant follow-up actions diff --git a/pm-toolkit/skills/notion/notion-learnings.md b/pm-toolkit/skills/notion/notion-learnings.md new file mode 100644 index 0000000..4cf22d1 --- /dev/null +++ b/pm-toolkit/skills/notion/notion-learnings.md @@ -0,0 +1,188 @@ +# Notion Skill — Living Knowledge Base + +This file grows automatically. Every time a fallback chain is triggered or a new pattern is discovered, an entry is appended here. Manual entries via `/notion learn` are also welcome. + +**Rules:** Append-only. Never delete entries. If this file exceeds 200 lines, consolidate duplicates but preserve all unique findings. + +--- + +### 2026-04-14: [pitfall] update_section TypeError on large pages + +`mcp__notion__update_section` throws `Cannot read properties of undefined (reading 'trim')` on the V4 PRD page (~21 sections, 100+ blocks, complex table structures). The error is consistent regardless of heading targeted or content length. + +**Dangerous side effect:** The tool partially deletes content on failure without writing the replacement. The section between the matched heading and the next heading gets removed, but the new content is never inserted. + +**Workaround:** Use the heading-match trick with the alternate MCP's `update_content` command instead (see below). +**Applies to:** `mcp__notion__update_section` + +--- + +### 2026-04-14: [workaround] Heading-match trick for inserting content before a block + +When you need to insert content before a heading (e.g., restore deleted section content), match the heading as `old_str` and replace with `[new content]\n### Heading Name` using the alternate MCP: + +``` +mcp__78651ef0...notion-update-page( + command: "update_content", + old_str: "### Two-Component Home Model (D78)", + new_str: "### 9.7 Onboarding\n[intro + tables]\n### Two-Component Home Model (D78)" +) +``` + +This works because headings are single blocks and `update_content` can match them individually. The trick effectively "inserts before" by including the original heading at the end of the replacement. + +**Applies to:** `mcp__78651ef0...notion-update-page` with `update_content` command + +--- + +### 2026-04-14: [pitfall] find_replace cannot match table row markdown + +`mcp__notion__find_replace` cannot match `| col1 | col2 | col3 |` strings because Notion stores table cells as separate blocks internally. The markdown `|`-delimited representation is generated by the read tool but doesn't exist as a searchable string. + +**Workaround:** For unique cell content, search for just the cell text (without pipes). For non-unique cell content, rewrite the entire table section. +**Applies to:** `mcp__notion__find_replace`, `mcp__78651ef0...notion-update-page update_content` + +--- + +### 2026-04-14: [pitfall] find_replace "Multiple matches found" with no first-match option + +When target text appears in multiple locations (e.g., `—` in two table cells), `find_replace` returns "Multiple matches found" and refuses to proceed. There is no "replace first match only" option — you must use `replace_all: true` (changes all) or make the match unique. + +**Workaround:** Either use `replace_all: true` if all instances should change, or use longer surrounding context to make `old_str` unique via the alternate MCP's `update_content`. +**Applies to:** `mcp__notion__find_replace` + +--- + +### 2026-04-14: [pitfall] Pipe characters inside table cells split the cell + +Using `|` inside a table cell value (e.g., `property_detail_update | utility_issue`) causes Notion to interpret it as a cell separator, truncating the cell. + +**Workaround:** Use `/` instead of `|` for enum notation inside table cells (e.g., `property_detail_update / utility_issue`). +**Applies to:** All Notion MCP tools that write tables + +--- + +### 2026-04-14: [pitfall] create_page 100-block limit + +`mcp__notion__create_page` rejects content with more than 100 blocks. Each paragraph, heading, list item, divider, code block, and table row counts as approximately one block. + +**Workaround:** Split content at a natural heading boundary near block 80-90. Create the page with the first chunk, then use `mcp__notion__append_content` for remaining chunks. +**Applies to:** `mcp__notion__create_page` + +--- + +### 2026-04-14: [pitfall] replace_content with escaped strings breaks formatting + +Using `replace_content` with large content strings causes `\n` to render as literal text, `\|` to show as escaped pipes, and tables to collapse into unreadable single blocks. + +**Workaround:** Never use `replace_content` for large content. For full rewrites, create a new page instead. For updates, use targeted `update_content` edits. +**Applies to:** `mcp__78651ef0...notion-update-page` with `replace_content` command + +--- + +### 2026-04-14: [pitfall] update_content cannot match across block boundaries + +The alternate MCP's `update_content` command matches text within single blocks only. A `\n` between two separate Notion blocks (e.g., a paragraph followed by a heading) will not match because they are stored as independent block objects. + +**Workaround:** Match content within a single block. For cross-block operations, use the heading-match trick or section-level rewrites. +**Applies to:** `mcp__78651ef0...notion-update-page` with `update_content` command + +--- + +### 2026-04-14: [optimization] Alternate MCP update_content is the most reliable write tool + +When primary MCP tools fail (`update_section` TypeError, `find_replace` table issues), the alternate MCP's `update_content` with `old_str`/`new_str` pairs consistently works. It operates on full page content server-side even when `notion-fetch` truncates the read output. + +**Pattern:** Default to `update_content` for any targeted edit where `find_replace` has known limitations. +**Applies to:** `mcp__78651ef0...notion-update-page` with `update_content` command + +--- + +### 2026-04-14: [behavior-change] Primary MCP disconnects mid-session + +The primary Notion MCP (`mcp__notion__*`) can disconnect during long sessions. When this happens, all `mcp__notion__*` tools become unavailable. The alternate MCP (`mcp__78651ef0...`) typically remains connected. + +**Workaround:** Run `/notion diagnose` to check MCP status. When primary is down, route all operations through the alternate MCP equivalents. +**Applies to:** All `mcp__notion__*` tools + +--- + +### 2026-04-14: [pitfall] notion-fetch truncates large pages + +The alternate MCP's `notion-fetch` tool truncates output on very large pages (observed at ~54KB). Content near the end of the page may be missing from the returned text. + +**Workaround:** Use `mcp__notion__read_page` (primary MCP) for reading — it returns full content without truncation. Fall back to `notion-fetch` only when primary is disconnected. +**Applies to:** `mcp__78651ef0...notion-fetch` + +--- + +### 2026-04-14: [optimization] Skill verification — diagnose, read, search all operational + +Both MCPs confirmed connected and functional via `/notion diagnose`. Primary MCP `read_page` returns full content without truncation. Primary MCP `search` returns comprehensive results. Alternate MCP `notion-search` returns workspace search results. All fallback chains are available. + +**Pattern:** Baseline verification after skill installation — confirms routing is normal. +**Applies to:** All tools + +--- + +### 2026-04-14: [behavior-change] update_section TypeError is page-specific, not a universal block count threshold + +`mcp__notion__update_section` failed consistently on the V4 PRD page (estimated 200+ blocks, 21+ sections, deeply nested tables). However, the same tool succeeded cleanly on the 03-09 Access Handoff page (~170 blocks, 20 sections, simple flat tables) — all 4 section rewrites succeeded with no TypeError. + +**Pattern:** Try `update_section` first. The TypeError is triggered by deep block nesting and complex table structures, not raw block count alone. If it TypeErrors, immediately re-read the page (partial deletion risk) and fall back to heading-match trick. +**Applies to:** `mcp__notion__update_section` + +--- + +### 2026-04-14: [pitfall] append_content adds a new table, not a row to an existing table + +Using `mcp__notion__append_content` with pipe-delimited markdown (e.g., `| D_AH6 | ... |`) when the last element on the page is a table creates a NEW single-row table block, not an additional row in the existing table. + +**Workaround:** Use `mcp__notion__update_section` to rewrite the entire section containing the table, including all existing rows plus the new row(s). +**Applies to:** `mcp__notion__append_content` + +--- + +### 2026-04-19: [pitfall] replace_content on parent pages silently archives all child pages + +When using `mcp__notion__replace_content` on a page that has child pages, ALL child pages become archived/orphaned. `list_pages` no longer shows them. They still exist but must be individually restored via `restore_page`. + +In the WhatsApp Wiki session, 17 out of 19 child pages were archived. Required 3 extra API round-trips to discover the problem, restore pages, and then proceed with the actual work. + +**Workaround:** NEVER use `replace_content` on pages that have children. Use `update_section` or `find_replace` for targeted edits on hub/parent pages. Reserve `replace_content` for leaf pages only. Before any write on a parent page, run `list_pages` to confirm child count. +**Applies to:** `mcp__notion__replace_content` + +--- + +### 2026-04-19: [pitfall] find_replace fails silently on text inside Notion table cells + +`find_replace` returned "No matches found" when trying to replace "92+" with "94+" inside a table cell on the WhatsApp Wiki hub page — even though the text was visually present in the read output. + +This is distinct from Pitfall P2 (table row markdown). Here the individual CELL text was targeted (without pipes), but it still failed. Table cells appear to have different internal storage than regular rich text blocks. + +**Workaround:** For any text change inside a table, use `update_section` to rewrite the entire section containing the table. `find_replace` should only be trusted on paragraphs, headings, callouts, and list items — never on table cell content. +**Applies to:** `mcp__notion__find_replace` + +--- + +### 2026-04-19: [optimization] Batch update protocol for multi-page operations + +When updating N child pages (e.g., adding a Parameters column to 19 pages), the following protocol was effective: + +1. **Pre-flight:** `list_pages(parent_id)` to check what's live vs archived +2. **Restore:** `restore_page` on any missing pages (compare against expected list) +3. **Sample read:** Read 1-2 representative pages to confirm current structure +4. **Batch process:** Read + write in batches of 3-4 pages (parallel reads, then parallel writes) +5. **Verify:** Read back 1 page per batch to confirm tables rendered correctly + +**Pattern:** Batches of 3-4 balance throughput vs context window usage. Reading all 19 at once overflows context; doing them 1-by-1 is slow. +**Applies to:** Any operation touching 3+ Notion pages + +--- + +### 2026-04-19: [optimization] Local page ID index file prevents cross-session ID loss + +Page IDs scattered in conversation context get lost during context compression and session breaks. Created a local `docs/notion-page-index.md` file mapping every page ID to its title, parent, and section. + +**Pattern:** After creating any Notion pages, immediately write/update a local index file. Read this file at the start of any Notion session. Format: `| ID | Title | Parent/Section |` +**Applies to:** All multi-page Notion structures diff --git a/pm-toolkit/skills/notion/notion-operations-guide.md b/pm-toolkit/skills/notion/notion-operations-guide.md new file mode 100644 index 0000000..6c72b15 --- /dev/null +++ b/pm-toolkit/skills/notion/notion-operations-guide.md @@ -0,0 +1,220 @@ +# Notion Operations Guide + +Quick-reference for tool selection, known pitfalls, and workarounds. Loaded by `/notion` skill during write operations. + +--- + +## 1. Tool Inventory + +### Primary MCP: `mcp__notion__*` + +| Tool | Purpose | +|------|---------| +| `read_page` | Read full page as markdown (no truncation) | +| `create_page` | Create page from markdown (100-block limit) | +| `append_content` | Append markdown to existing page | +| `find_replace` | Find/replace text (requires globally unique match) | +| `update_section` | Replace section by heading name | +| `replace_content` | Replace entire page content | +| `update_page` | Update title, icon, or cover | +| `search` | Search pages and databases | +| `list_pages` | List child pages under parent | +| `duplicate_page` | Duplicate a page | +| `archive_page` / `restore_page` | Archive or restore | +| `move_page` | Move page to new parent | +| `share_page` | Get shareable URL | +| `add_comment` / `list_comments` | Page comments | +| `get_database` | Get database schema | +| `query_database` | Query with filters and sorts | +| `add_database_entry` / `add_database_entries` | Create entries (single/bulk) | +| `update_database_entry` / `delete_database_entry` | Update/delete entries | +| `create_database` | Create new database | +| `list_databases` | List accessible databases | + +### Alternate MCP: `mcp__78651ef0-b949-45c5-8b08-0f97b6f05863__notion-*` + +**Note:** The UUID prefix may change if the MCP reconnects. Match by tool name suffix (e.g., `notion-update-page`, `notion-fetch`). + +| Tool | Purpose | +|------|---------| +| `notion-fetch` | Fetch page in enhanced markdown (may truncate large pages) | +| `notion-update-page` | Versatile update with multiple commands (see below) | +| `notion-create-pages` | Create pages with properties | +| `notion-search` | Search with advanced filters (date range, created_by) | +| `notion-duplicate-page` | Duplicate page | +| `notion-move-pages` | Bulk move pages | +| `notion-create-view` / `notion-update-view` | View CRUD (table, board, calendar, etc.) | +| `notion-update-data-source` | Schema management (add/rename/drop columns) | +| `notion-create-comment` / `notion-get-comments` | Rich text comments with threading | +| `notion-get-teams` / `notion-get-users` | Team and user listing | + +**`notion-update-page` commands:** + +| Command | What it does | +|---------|-------------| +| `update_content` | Find/replace with `old_str`/`new_str` pairs — matches within single blocks | +| `replace_content` | Replace entire page (warns about child page deletion) | +| `update_properties` | Update page properties (title, etc.) | +| `apply_template` | Apply a template to the page | +| `update_verification` | Set verification status | + +--- + +## 2. Tool Selection Matrix + +| Operation | Best Tool | Fallback | Known Limits | +|-----------|-----------|----------|-------------| +| **Read page** | `read_page` (primary) | `notion-fetch` (alt) | Alt truncates at ~54KB | +| **Create page (<90 blocks)** | `create_page` (primary) | `notion-create-pages` (alt) | 100-block limit | +| **Create page (>90 blocks)** | `create_page` + `append_content` | Split at heading boundary | Estimate blocks first | +| **Replace unique text** | `find_replace` (primary) | `update_content` (alt) | Must be globally unique | +| **Replace all instances** | `find_replace` + `replace_all: true` | — | All instances change | +| **Replace non-unique text** | `update_content` (alt) with context | Section rewrite | Can't cross block boundaries | +| **Rewrite section** | `update_section` (primary) | `update_content` heading trick (alt) | TypeError on large pages | +| **Insert before a block** | `update_content` heading trick (alt) | — | Match heading, prepend in replacement | +| **Edit unique table cell** | `find_replace` on cell text (no pipes) | Section rewrite | Cell text must be globally unique | +| **Edit non-unique table cell** | Section rewrite | — | Rewrite entire table under heading | +| **Search pages** | `search` (primary) | `notion-search` (alt, advanced) | Alt has date/user filters | +| **Database query** | `query_database` (primary) | — | Use `get_database` for schema first | +| **Database schema change** | `notion-update-data-source` (alt) | — | Primary has no schema tool | +| **View management** | `notion-create-view` / `update-view` (alt) | — | Primary has no view tools | + +--- + +## 3. Known Pitfalls + +### P1: update_section TypeError +**Symptom:** `Cannot read properties of undefined (reading 'trim')` on any heading. +**Trigger:** Large pages with complex block structures (observed on pages >100 blocks). +**Danger:** Partially deletes section content without writing replacement. +**Mitigation:** ALWAYS re-read page after a TypeError. Use heading-match trick as fallback. + +### P2: find_replace table cell failure +**Symptom:** "No matches found" for `| cell1 | cell2 |` syntax. +**Cause:** Notion stores table cells as separate blocks. The pipe-delimited markdown is a read-time representation, not stored text. +**Mitigation:** Search for cell text only (without pipes). If non-unique, rewrite the entire table section. + +### P3: find_replace "Multiple matches found" +**Symptom:** Error when target text appears in 2+ locations. +**Cause:** No "first match only" option. Tool requires uniqueness or explicit `replace_all: true`. +**Mitigation:** Use `replace_all: true` if all should change. Otherwise, add surrounding context to make match unique via alt MCP's `update_content`. + +### P4: update_content cross-block matching +**Symptom:** "No matches found" for text that spans two blocks (e.g., paragraph + heading separated by `\n`). +**Cause:** Blocks are independent objects. `\n` between blocks isn't part of either block's text. +**Mitigation:** Match within a single block only. For cross-block operations, use the heading-match trick. + +### P5: create_page 100-block limit +**Symptom:** `body.children.length should be <= 100, instead was N`. +**Cause:** Hard limit on blocks per API call. +**Mitigation:** Estimate blocks before creating. Split at heading boundary near block 80-90, create + append remaining chunks. + +### P6: replace_content formatting breakage +**Symptom:** `\n` renders as literal text, tables collapse, pipes show as `\|`. +**Cause:** Large escaped content strings don't parse correctly. +**Mitigation:** Never use `replace_content` for large content. Create a new page instead, or use targeted `update_content` edits. + +### P7: Pipe characters in table cells +**Symptom:** Cell content truncated at first `|` character. +**Cause:** Notion interprets `|` inside a cell value as a cell separator. +**Mitigation:** Use `/` instead of `|` for enum notation (e.g., `option_a / option_b`). + +### P8: Em-dash matching +**Symptom:** "No matches found" when searching for text containing `—`. +**Cause:** Em-dash (U+2014 `—`) and regular dash/hyphen (U+002D `-`) are different characters. +**Mitigation:** Copy the exact character from the read output. When in doubt, read the page first and use the exact string. + +### P9: notion-fetch truncation +**Symptom:** Content near the end of a large page is missing from fetch output. +**Cause:** Alt MCP truncates at ~54KB. +**Mitigation:** Use `read_page` (primary) for reading. Fall back to `notion-fetch` only when primary is disconnected. + +### P10: Primary MCP disconnection +**Symptom:** All `mcp__notion__*` tools fail. +**Cause:** MCP server disconnects during long sessions. +**Mitigation:** Run `/notion diagnose` to check status. Route through alt MCP equivalents. + +### P11: replace_content on parent pages archives all children +**Symptom:** After `replace_content` on a parent page, `list_pages` returns fewer children than expected. Child pages return "Could not find block" on read. +**Cause:** `replace_content` deletes ALL blocks including child page link blocks. The child pages become archived/orphaned. +**Mitigation:** NEVER use `replace_content` on pages with children. Use `update_section` or `find_replace` for targeted edits. Before writing to any page, check if it has children via `list_pages`. + +### P12: find_replace fails on table cell content (even unique text) +**Symptom:** "No matches found" when targeting text inside a table cell, even when the text is unique and copied exactly from read output. +**Cause:** Table cells have different internal storage from rich text blocks. Even individual cell text (without pipe delimiters) may not be matchable. +**Mitigation:** For any text change inside a table, use `update_section` to rewrite the entire section containing the table. Only trust `find_replace` on paragraphs, headings, callouts, and list items. + +--- + +## 4. Table Operations Deep Dive + +### How Notion Stores Tables + +Internally, a Notion table is a `table` block containing `table_row` child blocks. Each row has a `cells` array where each cell is an array of rich text objects. The markdown `| col1 | col2 |` representation is generated by the read tools — it does not exist as searchable text. + +**Implications:** +- `find_replace` and `update_content` cannot match across cells +- Each cell's text content IS individually searchable (without pipes) +- Identical cell values in different rows cause "Multiple matches found" +- Table rows are ~1 block each for block-count estimation + +### Table Edit Decision Tree + +``` +NEED: Edit a specific table cell + | + ├─ Cell content is globally unique on page? + | YES → find_replace(cell_text, new_text) + | NO → ↓ + | + ├─ ALL cells with this content should change? + | YES → find_replace(cell_text, new_text, replace_all=true) + | NO → ↓ + | + └─ Rewrite entire table section + 1. Identify the heading above the table + 2. Use SECTION mode to rewrite heading content (includes full table) +``` + +--- + +## 5. Block Count Estimation + +| Element | Blocks | +|---------|--------| +| Paragraph | 1 | +| Heading (any level) | 1 | +| List item | 1 per item | +| Table | 1 (table block) + 1 per row (including header) | +| Code block | 1 | +| Divider (`---`) | 1 | +| Callout / Quote | 1 | +| Toggle block | 1 | +| Image / Embed | 1 | + +**Quick estimate:** Count lines in your markdown (excluding blank lines), then add extra for table rows. If estimate > 85, plan to split. + +--- + +## 6. The Heading-Match Trick + +**Purpose:** Insert content BEFORE an existing heading when no other tool can do it. + +**How it works:** +1. Read the page to find the exact heading text (e.g., `### Two-Component Home Model (D78)`) +2. Use the alt MCP's `update_content` command +3. Set `old_str` to the heading text +4. Set `new_str` to: `[content you want to insert]\n### Two-Component Home Model (D78)` +5. The heading is preserved at the end; your content appears before it + +**Why it works:** Headings are single blocks with exact, usually unique text. By matching the heading and replacing it with "new content + original heading", you effectively insert before the heading. + +**When to use:** +- Content was accidentally deleted by a failed `update_section` call +- You need to add a new section between two existing sections +- There's no heading above your target insertion point + +**Limitations:** +- The heading text must be globally unique on the page +- New content in `new_str` is limited to what fits in a single API call +- If the heading contains special characters, copy them exactly from the read output