AgentStack
Browse Sign in
Browse Why AgentStack Sell Docs
Sign in
SKILL verified MIT Self-run

Mcp Notion

skill-michelkerkmeester-skilled-harness-spec-driven-agent-loops-mcp-notion · by MichelKerkmeester

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…

No reviews yet
0 installs
28 views
0.0% view→install

Install

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

✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access No
  • Filesystem access Used
  • Shell / process execution No
  • Environment & secrets Used
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

View the full security report →

Verified badge

Passed review? Show it. Paste this badge into your README, it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/skill-michelkerkmeester-skilled-harness-spec-driven-agent-loops-mcp-notion)

Reliability & compatibility

Security review passed
0 installs to date
no reviews yet
23d ago

Declared compatibility

Claude CodeClaude Desktop

Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.

Preview Execution monitoring

We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.

How agent discovery & health will work →
Are you the author of Mcp Notion? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

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

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

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:

{
  "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):

// 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.

Reviews

No reviews yet, be the first.

Versions

  • v0.1.0 Imported from the upstream source.