config/claude/skills/notion-rest-tricks/SKILL.md
Use this skill when working with the token-based Notion MCP servers (e.g. `mcp__notion-wife__API-*`) that wrap the raw Notion REST API — distinct from the OAuth `mcp__notion__*` server covered by notion-tricks. Covers native API JSON property shapes, the two-step page-then-content creation flow, and using `curl` with the `NOTION_TOKEN_*` env var to bypass MCP entirely for bulk/precise operations.
npx skillsauth add alexpopov/dots notion-rest-tricksInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
For token-based MCP servers built on @notionhq/notion-mcp-server (e.g. notion-wife). These expose the raw Notion REST API: tools are named API-post-page, API-query-data-source, API-patch-block-children, etc.
If you're using the OAuth mcp__notion__* server (which speaks Notion-flavoured Markdown), see notion-tricks instead. The two skills overlap deliberately — pick the one that matches the server you're calling.
The OAuth server lets you write properties as Markdown frontmatter. The REST server does not. You must pass the native Notion API shape:
{
"Name": { "title": [{"type": "text", "text": {"content": "20th Century"}}] },
"Tags": { "multi_select": [{"name": "Cocktail"}, {"name": "shaken"}] },
"Status": { "select": {"name": "Make Again"} },
"Rating": { "number": 4 },
"Link": { "url": "https://example.com" }
}
For multi/single select, pass option names by string — the API resolves them to existing options. If the option doesn't exist, the call fails (see "Adding new select options" below).
API-post-page's children field is declared as array of strings in the MCP wrapper, which is awkward to use. The reliable pattern is:
API-post-page — set parent, icon, properties. Capture the returned id.API-patch-block-children — pass block_id (the page id) and an array of block objects.The API-patch-block-children wrapper schema currently only types paragraph and bulleted_list_item block types, but the underlying API accepts the full set (heading_1/2/3, numbered_list_item, to_do, code, etc.). If the wrapper rejects something, fall back to curl (see below).
API-update-a-block for block contentDon't use this tool for editing block text — it's a confirmed bug in @notionhq/notion-mcp-server. The OpenAPI spec names the body field type, and http-client.ts forwards the parameter as body.type instead of spreading it at body root. The Notion API expects the block-type key (bulleted_list_item, paragraph, …) at body root and rejects with: "body.type should be not present".
No invocation form fixes this — passing type: {"bulleted_list_item": {...}} or type: {"rich_text": [...]} both fail. The only operation that works through the tool is the archived: true|false toggle (because archived maps to the correct body field directly).
Workaround: hit the API with curl instead.
curl -fsS -X PATCH "https://api.notion.com/v1/blocks/<block_id>" \
-H "Authorization: Bearer $NOTION_TOKEN_WIFE" \
-H "Notion-Version: 2025-09-03" \
-H "Content-Type: application/json" \
-d '{"bulleted_list_item":{"rich_text":[{"type":"text","text":{"content":"new text"}}]}}'
Source verified: see parser.ts:420–428 (flattens requestBody.type into a tool param) and http-client.ts:118–134 (passes the param object literally into the body) in https://github.com/makenotion/notion-mcp-server.
Tracked upstream as makenotion/notion-mcp-server#271 (open as of 2026-04-16, confirmed by another reporter 2026-04-20). No fix PR yet; check the issue before assuming the curl workaround is still needed.
database_id for single-source databasesAPI-post-page's parent uses one of three forms. For databases:
{ "type": "database_id", "database_id": "<uuid>" }
For multi-source databases (rare), the API can also accept data_source_id:
{ "type": "data_source_id", "data_source_id": "<uuid>" }
You'll see both IDs in API-post-search results — the data_source_id is the inner collection, the database_id is the wrapping page. Both parent.database_id and parent.data_source_id work for single-source DBs; the wrapper's schema field is named database_id even when it represents a data source, which is confusing. When in doubt, try database_id first.
API-post-search does title-only matching across the workspace. Good for finding a page or database by name.API-query-data-source is the powerful one — supports filters, sorts, and pagination over a specific data source. Use this to look inside a database:{
"data_source_id": "8ed55d29-7075-4df6-afcf-f49acd9f72a3",
"filter": {"property": "Name", "title": {"contains": "20th"}},
"page_size": 10
}
Filter syntax follows the Notion API — see https://developers.notion.com/reference/post-database-query-filter.
Same trap as the OAuth server: passing an unknown option name to API-post-page fails. To add a new option, API-update-data-source requires the full existing options list plus the new one — otherwise existing options get wiped:
{
"data_source_id": "<uuid>",
"properties": {
"Tags": {
"multi_select": {
"options": [
{"name": "Existing 1", "color": "red"},
{"name": "Existing 2", "color": "blue"},
{"name": "New Tag", "color": "green"}
]
}
}
}
}
The dataset of existing options can be huge (Cay's Recipes Tags has ~200). For workspaces where this matters, write a helper script that fetches current options, appends, and PATCHes — see notion-recipes for an example.
For bulk reads, schema-only fetches, or block types the MCP wrapper doesn't expose, hit the API directly:
curl -fsS https://api.notion.com/v1/data_sources/<id> \
-H "Authorization: Bearer $NOTION_TOKEN_WIFE" \
-H "Notion-Version: 2025-09-03" \
| jq '.properties.Tags.multi_select.options[].name'
This is much smaller than the corresponding MCP response (which embeds schema, sample rows, etc.) and is the right tool for "just give me the tag list".
The token env var is configured in config/claude-mcp/servers.json — currently NOTION_TOKEN_WIFE. Each token-based server maps to one workspace; the same workspace_id appears in API-get-self's response.
API-get-self returning 401 unauthorized usually means the token env var was empty or stale when Claude Code launched. MCP servers capture env at startup, so:
/mcp alone won't pick up parent-process env changes).The ${VAR} substitution in servers.json is performed by the launcher described in the recent claude-with commits — verify the var is exported in the parent shell.
API-post-search first to find a page/database by name, then API-retrieve-a-data-source to read schema, then targeted API-query-data-source for actual data.curl | jq for surgical extraction.API-get-self first when a server seems broken — it confirms auth and identifies the workspace.development
Backfill EXIF on Alp's film-scan JPEGs — camera, lens, sequenced DateTimeOriginal, and GPS — so they sort and identify correctly when imported into Photos.app. Use when the user wants to set/fix camera, lens, date, or location on a folder of images (typically under `~/Library/Mobile Documents/com~apple~CloudDocs/Pictures/To Import/`).
devops
Retroactively edit location, date, or timezone on photos that already live inside macOS Photos.app (and iCloud Photos) — including applying a GPX track. Use when the user wants to geotag, fix a wrong-TZ camera clock, shift timestamps, or apply GPS coordinates to photos that are NOT loose files but are inside Photos.app. Distinct from the `photo-exif` skill, which edits loose JPEG/RAW files before import.
tools
Use when the user says "open", "show me", "nvr", "open in editor", "open in nvr", "open in neovim", or wants to view a file in their editor rather than in the CLI. Also use proactively after creating or editing a file the user will want to review.
tools
Use this skill when adding, querying, or updating recipes in Cay's Notion (the `notion-wife` MCP). Covers the Recipes data source schema, tag conventions, the `recipes-helper.sh` script for fast tag/recipe operations via curl, and the rule that new tags must be confirmed with the user before creation.