UNPKG

notion-mcp-server

Version:

Notion MCP server with token auth: connect Claude Code, Claude Desktop, Cursor, VS Code, Codex and any MCP client to Notion. Search, create and edit pages, query databases, comment and upload files. Runs headless in CI and agents.

63 lines (62 loc) • 2.26 kB
import { z } from "zod"; const BARE_ID = /^[0-9a-f]{32}$/i; // An id as it appears inside a URL: a 32-hex run, or a dashed uuid. const ID_IN_URL = /[0-9a-f]{32}|[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi; // `notion://` is the desktop app's deep-link scheme; same shape after the scheme. const URL_SCHEME = /^(https?|notion):\/\//i; function dashed(hex) { const s = hex.toLowerCase(); return `${s.slice(0, 8)}-${s.slice(8, 12)}-${s.slice(12, 16)}-${s.slice(16, 20)}-${s.slice(20)}`; } // A bare or dashed id, as a dashed uuid; undefined when the string is neither. function asId(s) { const bare = s.replace(/-/g, ""); return BARE_ID.test(bare) ? dashed(bare) : undefined; } // The last id embedded in a string. A page URL is `<title-slug>-<id>`, and a // slug or workspace segment could itself look like hex, so the last run wins. function lastIdIn(s) { const runs = s.match(ID_IN_URL); return runs ? asId(runs[runs.length - 1]) : undefined; } export function normalizeNotionId(value, kind = "object") { if (typeof value !== "string") return value; const trimmed = value.trim(); const direct = asId(trimmed); if (direct) return direct; if (!URL_SCHEME.test(trimmed)) return value; let url; try { url = new URL(trimmed); } catch { return value; } if (kind === "block") { const anchor = lastIdIn(url.hash); if (anchor) return anchor; } if (kind === "view") { const view = asId(url.searchParams.get("v") ?? ""); if (view) return view; } // The path holds the object id. The query is dropped on purpose: a database // URL carries its view id in `?v=`, and a `database_id` field means the // database. return lastIdIn(url.pathname) ?? value; } /** * A Notion object id, in place of `z.string()`. * * `z.preprocess` rather than `.transform` on purpose: a transform emits `{}` * as its JSON Schema, which drops the type and the description from * notion_describe. Chained `.describe()` and `.optional()` both survive. */ export function notionId(kind = "object") { return z.preprocess((value) => normalizeNotionId(value, kind), z.string()); }