MCP tool reference
Penlog exposes six core tools over MCP at mcp.penlog.app, spoken
as JSON-RPC 2.0 over Streamable HTTP (POST only). Tools are invoked with
tools/call, passing the tool name and an
arguments object. (Two more — search and
fetch — exist for ChatGPT
compatibility.)
Calling convention
Each tool's result payload is JSON-encoded into a single MCP text content
block. Decode result.content[0].text to get the objects
documented below.
// tools/call request
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "list_pages",
"arguments": { "limit": 5 }
}
}
// result — the payload is JSON-encoded into a single text content block
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"content": [
{ "type": "text", "text": "{\"pages\":[ ... ]}" }
]
}
} Authentication & scopes
Connecting requires Penlog Pro. Authenticate with one-click
OAuth or a plg_live_ bearer token (see
Quickstart and
Remote MCP clients). Tokens carry one of three
scope presets; each tool declares the scope it needs:
| Preset | Scopes | Can call |
|---|---|---|
| Read | read | list_pages, get_page, search_pages, list_tasks |
| Read + Seed | read, seed | …the above, plus create_task_seed |
| Full | read, seed, write | …all six, including update_task |
Rate limits are per token: 60 requests/minute and 1000 requests/hour.
list_pages read
List journal pages, most recent first, optionally filtered by date range. Use it to discover which days have entries before fetching one.
Parameters
| Name | Type | Required | Notes |
|---|---|---|---|
from | string (date) | no | ISO YYYY-MM-DD, inclusive lower bound. |
to | string (date) | no | ISO YYYY-MM-DD, inclusive upper bound. |
limit | integer | no | 1–200, default 50. |
Arguments
{ "from": "2026-06-01", "to": "2026-06-19", "limit": 50 } Returns
{
"pages": [
{ "page_id": "0f9e…", "date": "2026-06-19", "has_ink": true, "has_ocr": true },
{ "page_id": "1a2b…", "date": "2026-06-18", "has_ink": true, "has_ocr": true }
]
} - has_ocr is true once the page has been OCR'd (an ocr_content_hash exists).
- Pages resolve within the user's single active journal (see Conventions).
get_page read
Fetch one day's full page: OCR text/markdown, the structured lines, and signed PDF/PNG image URLs.
Parameters
| Name | Type | Required | Notes |
|---|---|---|---|
page_id | string (uuid) | yes | From list_pages / search_pages. |
Arguments
{ "page_id": "0f9e8d7c-1234-…" } Returns
{
"page_id": "0f9e8d7c-1234-…",
"date": "2026-06-19",
"has_ink": true,
"ocr_text": "Follow up on API spec\nTeam sync notes",
"ocr_markdown": "- [ ] Follow up on API spec\n- Team sync notes",
"structure_json": {
"lines": [
{
"line_id": "ln_3",
"text": "Follow up on API spec",
"section": "tasks",
"notation": "bullet",
"status": "done",
"page_status": "open",
"status_source": "reconciled",
"completed_date": "2026-06-19",
"notion_url": "https://www.notion.so/…"
}
]
},
"pdf_url": "https://…(signed)",
"png_url": "https://…(signed)",
"signed_url_ttl_seconds": 3600
} - Task lines carry RECONCILED status: status is the Notion/Briefing-aware truth, page_status is the raw ink-derived status, and status_source: "reconciled" flags which lines were overlaid. Trust status over how the task looks on the page.
- pdf_url / png_url are signed and expire after signed_url_ttl_seconds (3600s). Re-fetch the page for fresh URLs.
- Returns not_found if the page_id isn't owned by the caller.
search_pages read
Full-text search across the user's pages by topic or keyword. Answers "what did I write about X."
Parameters
| Name | Type | Required | Notes |
|---|---|---|---|
query | string | yes | Non-empty search string. |
limit | integer | no | 1–100, default 20. |
Arguments
{ "query": "API spec", "limit": 20 } Returns
{
"query": "API spec",
"results": [
{
"page_id": "0f9e…",
"date": "2026-06-19",
"snippet": "…follow up on API spec with the team…"
}
]
} - Matches OCR'd text; the snippet is ~120 chars centered on the first match.
- Fetch the full page with get_page once you've found the right date.
list_tasks read
List tasks with reconciled status. Use it for "what's on my list" or "what's still open."
Parameters
| Name | Type | Required | Notes |
|---|---|---|---|
status | string | no | "todo" | "open" | "done". "open" is an alias for "todo". |
since | string (date) | no | ISO YYYY-MM-DD; tasks first seen on/after this date. |
limit | integer | no | 1–200, default 50. |
Arguments
{ "status": "open", "since": "2026-06-01", "limit": 50 } Returns
{
"tasks": [
{
"task_link_id": "7c3a…",
"text": "Follow up on API spec",
"status": "todo",
"first_seen_date": "2026-06-16",
"completed_date": null,
"source_page_id": "0f9e…",
"notion_url": "https://www.notion.so/…"
}
]
} - task_link_id is the identifier you pass to update_task.
- source_page_id is the page the task was handwritten on (null for agent-created tasks).
create_task_seed seed
Seed a task onto a target date's page — plan ahead or carry work forward — creating the page if it doesn't exist.
Parameters
| Name | Type | Required | Notes |
|---|---|---|---|
target_date | string (date) | yes | ISO YYYY-MM-DD. The page is created if missing. |
text | string | yes | 1–500 chars. Whitespace is normalized. |
source_url | string | no | Optional reference link stored alongside the task. |
Arguments
{ "target_date": "2026-06-20", "text": "Draft the launch post", "source_url": "https://…" } Returns
{
"task_link_id": "9d2f…",
"block_id": "b1c4…",
"page_id": "44a0…",
"created": true,
"reused_existing_task": false,
"notion_pushed": true,
"notion_task_id": "20e1…",
"notion_url": "https://www.notion.so/…"
} - Idempotent on normalized text per owner: an existing open task with the same text is reused (reused_existing_task: true) instead of duplicated.
- If the page is already seeded with this task, returns { created: false, reason: "already_seeded" }.
- If the target page already has ink, the block lands hidden and surfaces in the in-app Briefing as "pending" rather than dropping onto the canvas mid-writing.
- Notion push is best-effort: skipped cleanly when Notion isn't connected or the Tasks DB isn't provisioned yet (a later sync retries).
update_task write
Mark a task done/undone or edit its text, by task_link_id.
Parameters
| Name | Type | Required | Notes |
|---|---|---|---|
task_link_id | string (uuid) | yes | From list_tasks. |
status | string | no | "todo" | "done". Done stamps completed_date = today. |
text | string | no | 1–500 chars. Useful for fixing OCR transcription errors. |
Arguments
{ "task_link_id": "7c3a…", "status": "done" } Returns
{
"updated": true,
"status": "done",
"text": "Follow up on API spec",
"notion_updated": true
} - At least one of status or text must be provided.
- A no-op returns { updated: false, reason: "no_change" }.
- Syncs to Notion when the task is linked; the local on-page block text updates too.
ChatGPT compatibility — search & fetch
Two extra tools, search and fetch, exist specifically
for ChatGPT: it only accepts a connector that exposes both (otherwise the user
must enable Developer Mode), and its Deep Research path calls only these two.
They're thin read-scope aliases:
search({ query })→{ results: [{ id, title, url }] }— searches the same index assearch_pages.fetch({ id })→{ id, title, text, url, metadata }— returns one page's OCR'd text bypage_id, likeget_pagewithout the structured lines.
Both follow OpenAI's required schema (the result carries a
structuredContent object). If you're authoring against Penlog
directly, prefer search_pages + get_page — they
return richer data (snippets, structured lines, reconciled task status).
Errors
Failures return a JSON-RPC error with an HTTP status to match. Common ones:
| HTTP | JSON-RPC | message | When |
|---|---|---|---|
| 401 | -32001 | unauthorized | Missing or invalid bearer token. Carries a WWW-Authenticate header for OAuth discovery. |
| 402 | -32003 | entitlement_required | Owner isn't Pro. data.upgrade_url points to the upgrade path. |
| 403 | -32002 | missing_scope | Token lacks the scope the tool requires. |
| 429 | -32004 | rate_limit_exceeded | Over 60/min or 1000/hr. Honor the Retry-After header / data.retry_after_seconds. |
| 400 | -32602 | invalid_params | Malformed arguments (bad date, missing required field). |