Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
직접 명령은 검토 Prompt를 거치지 않습니다. 실행하기 전에 소스를 확인하세요.
npx skills add https://github.com/CodySwannGT/lisa --skill lisa-notion-access명령은 한 줄로 유지됩니다. 복사하기 전에 가로로 스크롤해 전체 내용을 확인하세요.
로컬 사본을 원하시나요? SkillsMP에서 현재 제공할 수 있는 파일을 다운로드하세요.
SKILL.md 표시 중
| name | lisa-notion-access |
| description | Vendor-neutral access layer for… |
| allowed-tools | ["Bash","Read","Skill"] |
Single chokepoint for all Notion operations. Routes each op to a substrate, enforces connection match, returns structured result. Caller skills (notion-*) MUST go through this — they MUST NOT call the Notion REST API or any mcp__*notion* tools directly.
operation: read-page id: <uuid>
operation: create-page parent_database_id: <uuid> properties: {...} [children: [...]] # create a new page (e.g. a PRD row) in a database; children is optional — omit to create a page without initial block content
operation: write-page payload: {...} # update page properties
operation: archive-page id: <uuid>
operation: query-database id: <uuid> filter: {...} sort: {...}
operation: read-database id: <uuid>
operation: append-blocks page_id: <uuid> children: [...]
operation: search query: "..." [filter: { object: "page" }]
operation: list-users
operation: get-self
The skill returns either the structured operation result (JSON) or an error message prefixed with Error: and a remediation hint.
Read config:
WORKSPACE=$(jq -r '.notion.workspaceId // empty' .lisa.config.json)
DB_ID=$(jq -r '.notion.prdDatabaseId // empty' .lisa.config.json)
[ -z "$WORKSPACE" ] && { echo "Error: notion.workspaceId not set. Run /lisa:setup:notion." >&2; exit 1; }
[ -z "$DB_ID" ] && { echo "Error: notion.prdDatabaseId not set. Run /lisa:setup:notion." >&2; exit 1; }
Probe each tier in order; the first that's ready AND identity-matches is the substrate for this operation. The ordering is the shared credential-substrate-precedence contract — the configured-provider token substrate leads, the interactive MCP is the fallback — not a Notion-local choice. Identity-match is verified before any operation; substrates authenticated as a different workspace are skipped, not used, at every tier.
substrate=""
# Tier 1: curl + API token — the configured-provider substrate, resolved through
# lisa-secrets-access. Leads because it is identical on a laptop, in CI, in a cloud
# routine, and in a subagent, and because its workspace binding travels with the
# request instead of coming from ambient browser-session state.
read_notion_token() {
local workspace="$1"
[ -n "$NOTION_API_TOKEN" ] && { echo "$NOTION_API_TOKEN"; return; }
local slug=$(echo "$workspace" | tr '[:upper:]-' '[:lower:]_')
local varname="NOTION_API_TOKEN_${slug}"
[ -n "${!varname}" ] && { echo "${!varname}"; return; }
# Preferred path: the single secrets chokepoint. It owns the one-store rule
# and the surface ladder, so anything it can answer must not be read out of an
# OS keychain here — a second reader is how the same credential ends up living
# in two places and drifting.
#
# Ordered across trusted machine-managed substrates, ending at the installed
# package. Checkout-local paths are deliberately absent: a familiar generated
# destination is still repository-controlled executable code. The plugin
# rungs are the floor: `resolve-secret.mjs` ships beside this skill, so a rung
candidates=()
[ -n ];
candidates+=()
[ -n ];
candidates+=()
candidates+=(node_modules/@codyswann/lisa/plugins/lisa/skills/lisa-secrets-access/scripts/resolve-secret.mjs)
resolver
tried=()
resolver ;
tried+=()
[ -f ];
via_lisa
via_lisa=$(node get NOTION_API_TOKEN 2>/dev/null) \
&& [ -n ] && { ; ; }
from_keychain=
Darwin) from_keychain=$(security find-generic-password -s lisa-notion -a -w 2>/dev/null) ;;
Linux) -v secret-tool >/dev/null && \
from_keychain=$(secret-tool lookup service lisa-notion account 2>/dev/null) ;;
MINGW*|MSYS*|CYGWIN*)
from_keychain=$(LISA_CRED_TARGET= powershell.exe -NoProfile -NonInteractive -Command 2>/dev/null | -d ) ;;
[ -n ] && { ; ; }
>&2
>&2
>&2
>&2
1
}
TOKEN=$(read_notion_token )
[ -n ];
me=$(curl -s -H -H \
)
me_workspace=$( | jq -r )
[ -n ] && [ = ];
substrate=
[ -n ];
>&2
mcp_notion_can_fetch_database ;
:
mcp_available=
[ -z ];
plugin_enabled_global=$(jq -r ~/.claude/settings.json 2>/dev/null || )
plugin_enabled_project=$(jq -r .claude/settings.json 2>/dev/null || )
plugin_enabled_local=$(jq -r .claude/settings.local.json 2>/dev/null || )
>&2 <<
1
The substrate selection in Step 1 already verifies identity. This step is the explicit re-assertion before any operation runs — defensive in case substrate state changed since selection. For the curl tier, re-validate token-to-workspace pairing if more than a few minutes elapsed.
The workspace identifier stored in config is whatever stable string the user picked at setup time — typically bot.workspace_name (human-readable) for simplicity. If the workspace has been renamed in Notion, setup-notion re-detects and re-stores; the access skill surfaces the mismatch instead of silently authing as the wrong workspace.
When $substrate=mcp, route through Notion MCP tools. When $substrate=curl, hit the Notion REST API directly. All curl calls use https://api.notion.com/v1/<path>, Notion-Version: 2022-06-28, Authorization: Bearer $TOKEN.
Substrate columns: try the column matching $substrate first. If that column is — for the requested operation (no adapter), fall through to the other substrate if it's also available. If neither has an adapter, the operation is unsupported.
| Operation | MCP adapter | curl adapter |
|---|---|---|
| Pages | ||
read-page id:<I> | mcp__claude_ai_Notion__notion-fetch | GET /v1/pages/<I> |
create-page parent_database_id:<D> properties:<P> [children:<arr>] | mcp__claude_ai_Notion__notion-create-pages | POST /v1/pages body { "parent": { "database_id": "<D>" }, "properties": <P>, "children": <arr?> } (children optional per Notion API) |
write-page payload:<P> | mcp__claude_ai_Notion__notion-update-page | PATCH /v1/pages/<I> body { "properties": {...}, "archived": true/false } |
archive-page id:<I> | mcp__claude_ai_Notion__notion-update-page (with archived: true) | PATCH /v1/pages/<I> body { "archived": true } |
append-blocks page_id:<P> children:<arr> | (no direct equivalent) | PATCH /v1/blocks/<P>/children body { "children": <arr> } |
| Databases | ||
read-database id:<I> | mcp__claude_ai_Notion__notion-fetch | GET /v1/databases/<I> |
query-database id:<I> filter:<F> sort:<S> | mcp__claude_ai_Notion__notion-search (with collection scope) | POST /v1/databases/<I>/query body { "filter": <F>, "sorts": <S>, "page_size": <N> } |
| Comments | ||
list-comments block_id:<I> | (MCP lacks a generic list-comments tool) | GET /v1/comments?block_id=<I> |
create-comment page_id:<I> rich_text:<arr> | mcp__claude_ai_Notion__notion-create-comment (page-level) |
Operations not in this table are unsupported — add an adapter row before invoking. Adapters MUST return parsed JSON; never raw HTTP responses.
Wrap the JSON response in a <result> block for caller parsing. On HTTP non-2xx, prefix the error message with Error: and surface the HTTP status code plus Notion's response body verbatim.
exec_op() {
local method="$1" path="$2" body="${3:-}"
local args=( -s -X "$method"
-H "Authorization: Bearer $TOKEN"
-H "Notion-Version: 2022-06-28" )
[ -n "$body" ] && args+=( -H "Content-Type: application/json" --data-binary "$body" )
local code=$(curl "${args[@]}" -o /tmp/notion-resp -w "%{http_code}" \
"https://api.notion.com/v1${path}")
if [ "${code:0:1}" != "2" ]; then
echo "Error: Notion API $method $path returned HTTP $code" >&2
cat /tmp/notion-resp >&2
return 1
fi
cat /tmp/notion-resp
}
curl https://api.notion.com/... or any mcp__*notion* tool directly. They invoke this skill via the Skill tool with an operation name and arguments.credential-substrate-precedence contract — internal-integration token first, Notion MCP as fallback. The first tier that's available AND identity-matches notion.workspaceId wins. Do not restate or locally override the ordering here./lisa:setup:notion.Notion-Version is pinned to 2022-06-28 — the version every existing notion-* skill targets. Bumping it is a coordinated change across the access skill and all callers.In a headless / non-interactive context (no TTY, CI=true, or -p mode), the MCP tier is unavailable (its OAuth flow needs a browser) and the ladder collapses to curl + NOTION_API_TOKEN — which is already tier 1 interactively. That is the point of the ordering: headless and interactive sessions take the same primary path, so a credential problem reproduces on a laptop instead of only in cron (credential-substrate-precedence, "headless parity"). Same skill code runs identically; only the availability of the fallback changes.
Notion integrations only see pages that have been explicitly shared with them. If read-page or query-database returns a 404 or object_not_found error and the configured workspace is correct, the cause is almost always that the page/database wasn't shared with the integration. Surface this in the error message:
Page not visible to the integration. Open the page in Notion → "..." menu → Connections → add the lisa integration.
Do not paper over with a retry. Sharing is a one-time human action per database (or per page if the user prefers page-level sharing); failures here mean the user needs to act.
POST /v1/comments body { "parent": { "page_id": "<I>" }, "rich_text": <arr> } |
create-comment-on-block block_id:<I> rich_text:<arr> | mcp__claude_ai_Notion__notion-create-comment (with block anchor) | POST /v1/comments body { "parent": { "block_id": "<I>" }, "rich_text": <arr> } |
| Search & users |
search query:<Q> [filter:<F>] | mcp__claude_ai_Notion__notion-search | POST /v1/search body { "query": "<Q>", "filter": <F or null> } |
list-users | — | GET /v1/users |
get-self | — | GET /v1/users/me |