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:

PresetScopesCan call
Readreadlist_pages, get_page, search_pages, list_tasks
Read + Seedread, seed…the above, plus create_task_seed
Fullread, 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

NameTypeRequiredNotes
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

NameTypeRequiredNotes
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

NameTypeRequiredNotes
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

NameTypeRequiredNotes
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

NameTypeRequiredNotes
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

NameTypeRequiredNotes
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 as search_pages.
  • fetch({ id }){ id, title, text, url, metadata } — returns one page's OCR'd text by page_id, like get_page without 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:

HTTPJSON-RPCmessageWhen
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).