# Mcp Notion

> Operates a Notion workspace through the official Notion MCP (@notionhq/notion-mcp-server, 24 tools) over Code Mode, fills 5 API gaps with direct Notion API calls, and routes between the headless local-stdio backend and the interactive remote OAuth backend. Carries a Notion knowledge layer (data-source model, 22 property types, relations, rollups, Formulas 2.0). Embedded registration and agent saf…

- **Type:** Skill
- **Install:** `agentstack add skill-michelkerkmeester-skilled-harness-spec-driven-agent-loops-mcp-notion`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [MichelKerkmeester](https://agentstack.voostack.com/s/michelkerkmeester)
- **Installs:** 0
- **Category:** [Productivity](https://agentstack.voostack.com/c/productivity)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [MichelKerkmeester](https://github.com/MichelKerkmeester)
- **Source:** https://github.com/MichelKerkmeester/skilled-harness__spec-driven-agent-loops/tree/main/.opencode/skills/mcp-tooling/mcp-notion

## Install

```sh
agentstack add skill-michelkerkmeester-skilled-harness-spec-driven-agent-loops-mcp-notion
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# mcp-notion Skill

Notion workspace operations via the **official Notion MCP** (`@notionhq/notion-mcp-server`, 24 tools) over Code Mode, plus **direct Notion API calls** for the five capabilities the MCP does not expose. It knows Notion's data model at the schema layer — data sources, 22 property types, relations, rollups, Formulas 2.0 — so an agent can create, query, and extend a workspace without guessing.

---

## MARKDOWN + DATA-SOURCE CONTRACT — READ BEFORE ANY NOTION WRITE

Two shape rules cause most Notion write failures:

1. **Page bodies vs page properties are different surfaces.** A page's *content* is a tree of **blocks** (append/update via block tools, or the markdown round-trip tools). A page's *metadata* is **properties** typed by its parent data source's schema. Writing prose into a property, or a property value into a block, fails or silently truncates.
2. **API 2.0 replaced "databases" with "data sources"** (API version `2025-09-03`). A database is now a container of one or more **data sources**; queries, schema, relations, and rollups all target a **data source id**, not a database id. Using a database id where a data-source id is required is the most common 400.

Markdown round-trip tools (`retrieve-page-markdown` / `update-page-markdown`) require API version `2026-03-11` and are the token-efficient path for AI. Full detail: `references/database-model.md` and `references/mcp-tools.md`.

**Failure symptom:** a `validation_error` naming `data_source_id`, or property writes that vanish, means a database id was used where a data-source id was required, or content was routed to the wrong surface.

---

## 1. WHEN TO USE

### Activation Triggers (explicit user phrases)

- "notion", "notion mcp", "notion api", "mcp-notion"
- "notion page", "notion database", "notion data source", "query notion"
- "create a notion page", "add a row", "notion property", "notion relation", "notion rollup", "notion formula"
- "notion token", "notion integration", "ntn_", "notion search"
- "upload a file to notion", "notion view", "notion async task"

### Automatic Triggers (keyword patterns)

- `notion` + any action verb (create, read, update, query, search, append, archive)
- "data source" / "property" / "rollup" / "relation" in a Notion context
- MCP tool names: `create-a-page`, `query-data-source`, `retrieve-a-page` (namespaced `notion.notion_`)

### When NOT to Use

- **Community Notion MCP servers** (suekou/mcp-notion-server, awkoy/notion-mcp-server) — this mode uses the **official** `@notionhq/notion-mcp-server` only.
- **Notion→Obsidian migration mechanics (running the importer itself)** — the importer runs inside the Obsidian desktop app, not here; this mode is the read-side inventory enabler (`references/migration-inventory.md`), not the importer.
- Generic markdown authoring with no Notion workspace — use `@markdown` / `sk-doc`.
- Non-Notion knowledge apps (Obsidian, ClickUp docs) — wrong surface (`mcp-obsidian`, `mcp-click-up`).

---

## 2. SMART ROUTING

### Resource Loading Levels

```
ALWAYS:    SKILL.md (this file)
ON_DEMAND: references/mcp-tools.md        (24-tool catalog + Code Mode invocation)
           references/api-gap-tools.md    (direct API for the 5 uncovered capabilities)
           references/property-types.md   (22 property types: schema, value, filter/sort)
           references/database-model.md    (data-source hierarchy, relations, rollups, Formulas 2.0)
           references/troubleshooting.md   (auth, rate-limit, version, deprecation-migration)
           references/migration-inventory.md (Notion→Obsidian migration read-side inventory method)
```

### Two Decisions This Router Makes

1. **MCP vs direct API** — anything in the 24-tool surface (page/block/data-source/comment/user/search CRUD) goes through the MCP over Code Mode; the five gaps (file uploads, views, page property items, async tasks, daily-notes convention) go to direct Notion API calls.
2. **Which backend** — headless **local stdio** (`npx @notionhq/notion-mcp-server`, `NOTION_TOKEN`) for Code Mode / automated sessions; **remote OAuth** (`mcp.notion.com`) only when an interactive browser session is available. Code Mode is headless, so it uses the local stdio backend.

### Backend Selection

```python
def resolve_notion_backend(runtime):
    """Pick the Notion backend. Probe, never assume.
      runtime.interactive   -> a browser/OAuth session is available
      runtime.oauth_token   -> remote Notion MCP OAuth completed
      runtime.notion_token  -> NOTION_TOKEN (ntn_...) present for Code Mode
    """
    # Remote MCP is OAuth-only and CANNOT run headless. Prefer it only when a
    # human/browser session is present; it adds async-task tools the local lacks.
    if runtime.interactive and runtime.oauth_token:
        return "REMOTE_MCP"      # https://mcp.notion.com/mcp (Streamable HTTP + OAuth)

    # Default for Code Mode and any automated session: the local stdio server.
    # Deprecated by Notion but the only headless-capable backend.
    if runtime.notion_token:
        return "LOCAL_STDIO"     # notion manual in .utcp_config.json, via Code Mode

    return "ESCALATE"            # no auth — direct the user to INSTALL-GUIDE.md
```

### Operation-to-Tool Routing Table

| Operation | Surface | Tool / call | Notes |
|---|---|---|---|
| Create / retrieve / update / archive a page | **MCP** | `create-a-page` / `retrieve-a-page` / `update-page-properties` / `archive-a-page` | Archive, not hard delete |
| Page body as markdown (read/write) | **MCP** | `retrieve-page-markdown` / `update-page-markdown` | Needs API `2026-03-11`; token-efficient |
| Append / update / delete blocks | **MCP** | block tools (`append-block-children`, `update-a-block`, `delete-a-block`, …) | Page *content* surface |
| Query / retrieve / update a data source | **MCP** | `query-data-source` + data-source tools | Target the **data-source id** |
| Comments (create, list) | **MCP** | comment tools | — |
| Users (list, retrieve, bot) | **MCP** | user tools | — |
| Search (by title) | **MCP** | `search` | **Title-only**; no full-text content search |
| **File uploads** | **direct API** | `POST /v1/file_uploads` (+ send/complete) | Not in MCP — see `api-gap-tools.md` |
| **Views** (create/list/query) | **direct API** | data-source view endpoints | Not in MCP |
| **Page property items** (non-truncated) | **direct API** | `GET /v1/pages/{id}/properties/{prop}` | Not in MCP |
| **Async tasks** (poll) | **direct API / remote MCP** | task-status endpoint | Native on remote MCP only |
| **Daily notes** | **convention** | knowledge-layer pattern | No API — see `database-model.md` |

### Smart Router Pseudocode

```python
from pathlib import Path

SKILL_ROOT = Path(__file__).resolve().parent
RESOURCE_BASES = (SKILL_ROOT / "references",)
DEFAULT_RESOURCE = "references/mcp-tools.md"
# Fallback-only: DEFAULT_RESOURCE is a defer-time suggestion, never unioned into a
# route's loaded set. Scored routes load exactly RESOURCE_MAP[intent]; zero-score
# routes load nothing and ask for disambiguation instead.
DEFAULT_RESOURCE_SEMANTICS = "fallback-only"

UNKNOWN_FALLBACK_CHECKLIST = [
    "Confirm whether the request is Notion page/block ops, data-source/schema ops, an API-gap capability, install/auth, or troubleshooting",
    "Provide the page id, data-source id, property name, or error text",
    "Confirm whether NOTION_TOKEN (headless/Code Mode) or a remote OAuth session is available",
    "Confirm the verification command before completing any write action",
]

INTENT_SIGNALS = {
    "NOTION_PAGES": {
        "weight": 5,
        "keywords": ["page", "pages", "block", "blocks", "append", "markdown", "content",
                     "create page", "retrieve page", "update page", "archive", "comment",
                     "user", "search", "title search", "sub-page", "child page"],
    },
    "NOTION_DATA": {
        "weight": 5,
        "keywords": ["database", "data source", "datasource", "query", "row", "rows",
                     "relation", "rollup", "formula", "schema", "property", "filter",
                     "sort", "data_source_id", "two-way relation", "aggregate"],
    },
    "NOTION_API_GAP": {
        "weight": 6,
        "keywords": ["file upload", "upload a file", "view", "views", "property item",
                     "non-truncated", "async task", "poll task", "daily note"],
    },
    "NOTION_KNOWLEDGE": {
        "weight": 5,
        "keywords": ["property type", "property types", "select", "multi-select", "status",
                     "formula function", "rollup function", "data model", "hierarchy"],
    },
    "NOTION_MIGRATION": {
        "weight": 5,
        "keywords": ["migration", "migrate", "migration inventory", "notion import",
                     "obsidian import", "workspace inventory", "pre-migration inventory",
                     "relation recovery", "rollup recovery", "comment reconstruction",
                     "parity verification"],
    },
    "INSTALL": {
        "weight": 6,
        "keywords": ["install", "setup", "not found", "not installed", "notion token",
                     "ntn_", "integration token", "api token", "mcp config", "register",
                     "configure", "configuration", "getting started", "onboarding",
                     "oauth", "connect notion", "how do i install"],
    },
    "TROUBLESHOOT": {
        "weight": 6,
        "keywords": ["error", "failed", "not working", "401", "403", "429", "400",
                     "rate limit", "unauthorized", "forbidden", "slow", "timeout",
                     "validation_error", "data_source_id", "deprecated", "won't connect",
                     "can't connect", "version mismatch", "object_not_found"],
    },
}

# NOTE: no "DEFAULT" entry — route_notion_resources() never indexes RESOURCE_MAP by
# that key; the selected `intent` is always one of the seven INTENT_SIGNALS keys. The
# no-match case is owned by DEFAULT_RESOURCE, whose fallback-only semantics mean it is
# SUGGESTED beside the disambiguation checklist, never loaded — so mcp-tools.md cannot
# leak into the DATA / API_GAP / KNOWLEDGE / MIGRATION / INSTALL / TROUBLESHOOT routes.
RESOURCE_MAP = {
    "NOTION_PAGES":     ["references/mcp-tools.md"],
    "NOTION_DATA":      ["references/database-model.md", "references/property-types.md",
                         "references/mcp-tools.md"],
    "NOTION_API_GAP":   ["references/api-gap-tools.md"],
    "NOTION_KNOWLEDGE": ["references/property-types.md", "references/database-model.md"],
    "NOTION_MIGRATION": ["references/migration-inventory.md"],
    "INSTALL":          ["references/troubleshooting.md"],
    "TROUBLESHOOT":     ["references/troubleshooting.md"],
}

def discover_markdown_resources() -> set[str]:
    docs = []
    for base in RESOURCE_BASES:
        if base.exists():
            docs.extend(path for path in base.rglob("*.md") if path.is_file())
    return {doc.relative_to(SKILL_ROOT).as_posix() for doc in docs}

def _guard_in_skill(relative_path: str) -> str:
    resolved = (SKILL_ROOT / relative_path).resolve()
    resolved.relative_to(SKILL_ROOT)
    if resolved.suffix.lower() != ".md":
        raise ValueError(f"Only markdown skill resources are routable: {relative_path}")
    return resolved.relative_to(SKILL_ROOT).as_posix()

def load_if_available(relative_path, loaded, seen, inventory) -> None:
    guarded = _guard_in_skill(relative_path)
    if guarded in inventory and guarded not in seen:
        load(guarded)
        loaded.append(guarded)
        seen.add(guarded)

def route_notion_resources(request: str) -> dict:
    """Score intent labels and load available Notion reference docs."""
    inventory = discover_markdown_resources()
    loaded, seen = [], set()
    request_lower = request.lower()

    scores = {}
    for intent, config in INTENT_SIGNALS.items():
        score = sum(config["weight"] for kw in config["keywords"] if kw in request_lower)
        if score > 0:
            scores[intent] = score

    if not scores:
        return {
            "load_level": "UNKNOWN_FALLBACK",
            "needs_disambiguation": True,
            "disambiguation_checklist": UNKNOWN_FALLBACK_CHECKLIST,
            "suggested_fallback": DEFAULT_RESOURCE,
            "resources": loaded,
        }

    # Error/install keywords win regardless of other signals.
    if scores.get("TROUBLESHOOT", 0) > 3:
        intent = "TROUBLESHOOT"
    elif scores.get("INSTALL", 0) > 4:
        intent = "INSTALL"
    else:
        intent = max(scores, key=scores.get)

    for resource in RESOURCE_MAP[intent]:
        load_if_available(resource, loaded, seen, inventory)

    if not loaded:
        return {
            "load_level": "UNKNOWN_FALLBACK",
            "notice": f"No Notion reference docs available for intent '{intent}'",
            "disambiguation_checklist": UNKNOWN_FALLBACK_CHECKLIST,
            "suggested_fallback": DEFAULT_RESOURCE,
            "resources": loaded,
        }

    return {"intent": intent, "resources": loaded}
```

---

## 3. HOW IT WORKS

### Backend Comparison

| Dimension | Local stdio (default, headless) | Remote MCP (interactive) |
|---|---|---|
| **Transport** | stdio via `npx -y @notionhq/notion-mcp-server` | Streamable HTTP at `https://mcp.notion.com/mcp` |
| **Auth** | `NOTION_TOKEN` (`ntn_…` internal-integration token) | OAuth (browser) |
| **Headless?** | **Yes** — the only Code-Mode-capable backend | No — interactive only |
| **Tool names** | `create-a-page`, `retrieve-a-page`, … | `notion-create-pages`, … + async tasks |
| **Status** | **Deprecated** by Notion, still functional | **Recommended** by Notion |
| **Used by** | Code Mode / automation | Human-in-the-loop clients |

> **Deprecation note.** Notion is deprecating the open-source local server in favor of the remote OAuth server. Code Mode is headless, so the local stdio server is the correct (and only) choice here today; `references/troubleshooting.md` carries the local→remote migration path for when the operator moves to an interactive workflow.

### Official Notion MCP — via Code Mode (default path)

The registered `notion` manual launches `@notionhq/notion-mcp-server` over **stdio** with `npx -y`. It talks to Notion's REST API with the integration's `NOTION_TOKEN`.

**Prerequisites:**
- Code Mode MCP configured, with the `notion` manual in `.utcp_config.json` (already registered — see INSTALL-GUIDE).
- `notion_NOTION_TOKEN` (an `ntn_…` internal-integration token) available to Code Mode, and the integration granted content access to the target pages/data sources.

**Configuration** (`.utcp_config.json`, `manual_call_templates`) — already applied:
```json
{
  "name": "notion",
  "call_template_type": "mcp",
  "config": {
    "mcpServers": {
      "notion": {
        "transport": "stdio",
        "command": "npx",
        "args": ["-y", "@notionhq/notion-mcp-server"],
        "env": { "NOTION_TOKEN": "${notion_NOTION_TOKEN}" }
      }
    }
  }
}
```

The `notion_` env prefix matches the manual name `notion`, so `${notion_NOTION_TOKEN}` resolves. This mode documents that registration; it does not rewrite config files.

**Tools:** the server exposes **24 `notion_*` tools** across 6 domains — pages (7), blocks (5), data sources (6), comments (2), users (3), search (1). Confirm every name with `tool_info()` / `list_tools()` before calling; the full catalog with per-tool inputs is in `references/mcp-tools.md`.

**Invocation via Code Mode** (`call_tool_chain` takes a single `code` string):
```typescript
// Code Mode namespaces each tool as notion.notion_. Notion tool names are
// HYPHENATED (create-a-page, retrieve-a-page), so notion.notion_retrieve-a-page is invalid
// JS (it parses as subtraction) — use hyphen-safe BRACKET access. VERIFY once the manual is
// registered: run list_tools() to read the exact callable; Code Mode MAY instead sanitize
// hyphens to underscores (notion.notion_retrieve_a_page). Bracket form is the safe default.
cons

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [MichelKerkmeester](https://github.com/MichelKerkmeester)
- **Source:** [MichelKerkmeester/skilled-harness__spec-driven-agent-loops](https://github.com/MichelKerkmeester/skilled-harness__spec-driven-agent-loops)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** yes
- **Shell / process execution:** no
- **Environment & secrets:** yes
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-michelkerkmeester-skilled-harness-spec-driven-agent-loops-mcp-notion
- Seller: https://agentstack.voostack.com/s/michelkerkmeester
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
