dorfteich/docs/self-hosting/public-api.md
Claude Fable 5 04e21a0aac
All checks were successful
CD / Build and push images (push) Successful in 3m50s
CI / Lint, typecheck, test (push) Successful in 4m2s
CI / Build container images (push) Has been skipped
CD / Deploy to Test (push) Successful in 11s
CD / Smoke tests against Test (push) Successful in 1m14s
CD / Promote to Int (push) Successful in 13s
CI / Auth e2e pack (push) Successful in 5m37s
CI / Import/export fidelity gate (push) Successful in 47s
Built-in MCP endpoint (Streamable HTTP) on top of the public API (#105)
AI clients talk to the instance directly at /api/mcp — under the /api/
path (deviation from the issue's literal /mcp) so every existing reverse
proxy already routes it; no deployment changes anywhere.

- Transport: official @modelcontextprotocol/sdk server, STATELESS — each
  POST builds a fresh server+transport pair, no session store, replicas
  stay trivial; GET/DELETE answer 405. Auth per PAT bearer (#104 tokens),
  per-token rate limit (429 + Retry-After).
- Own switches, independent of REST: instance mcp.enabled (admin
  settings, default off; off = 404, feature invisible) + pond setting
  mcpEnabled (pond-settings toggle, default off) — pinned independent in
  both directions by tests.
- Tools (thin wrappers over the #104 services, same permission gates,
  audit-logged writes): list_ponds, list_pages, read_page, search,
  create_page, update_page (replace semantics through the collab-owned
  restore path — open editors converge), add_comment, list_labels,
  set_page_labels (exact replace), export_pond (link to the REST ZIP).
  Tool errors carry the api error codes; results carry stable slugs/ids.
  MCP resources stay the documented stage-2 stretch goal.
- Deliberately on the SDK's low-level Server API with a hand-written tool
  table (mcp-tools.ts): the typed registerTool generics drove tsc out of
  memory in a program this size; manual Zod validation keeps the wire
  behavior explicit.
- PublicApiService exposure filtering parameterized ('api' | 'mcp',
  shared pondFeatureEnabled helper) — one implementation, two switches.
- Docs: "Connect Claude Code / MCP clients" section in public-api.md
  (claude mcp add one-liner + mcp-remote bridge for stdio clients).

Verification: 8-test e2e pack driving the real MCP SDK client over
Streamable HTTP against a listening api (initialize + tools/list, switch
independence in both directions, anonymous/garbage 401, opt-in 404
semantics, page roundtrip incl. restore-NOTIFY, labels/comments, read
scope blocked from writes with scope_required); live check through the
web proxy against the seeded stack (tools list, create, read, update,
search — LIVE CHECK PASSED); full api suite 61/61 files green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1
2026-07-12 11:36:02 +02:00

4.8 KiB

Public REST API (issue #104)

Dorfteich instances can expose a token-authenticated REST API at /api/public/v1 for scripts and integrations. It is off by default, twice: a Site Admin enables the instance switch (Admin → Settings → Public API), and every pond that should be reachable opts in separately (Pond settings → Public API). Anything not enabled answers 404 — indistinguishable from an instance without the feature.

Personal access tokens

Every user manages their tokens under Settings → API tokens: a name, a scope (read or read+write), an optional expiry, and an optional restriction to selected ponds. The secret (dt_pat_…) is shown exactly once and stored hashed; tokens are revocable and show their last use.

A token acts as its user: the normal permission model — grants, label scopes, the 404-vs-403 policy — applies unchanged. Scope and pond restriction only narrow it further; they never grant anything the user could not do in the app.

Using the API

curl -H "Authorization: Bearer dt_pat_..." \
  https://your-instance.example/api/public/v1/me
  • The machine-readable description lives at /api/public/v1/openapi.json (reachable without a token while the instance switch is on).
  • Requests are rate-limited per token (HTTP 429 + Retry-After).
  • Errors carry the api's uniform body: { code, message, details? }.
  • Page content is Markdown in, Markdown + rendered HTML out. A content update (PATCH …/pages/{slug} with markdown) replaces the whole page; it is applied through the collaborative document, so open editors converge live, and the previous state stays in the version history (the change itself appears as a manual version named "API update").

Endpoint overview

Area Endpoints
Identity GET /me
Ponds GET /ponds, GET /ponds/{slug}
Pages GET/POST /ponds/{slug}/pages, GET/PATCH/DELETE /ponds/{slug}/pages/{pageSlug}
Search GET /search?q=&pond=&label=
Export GET /ponds/{slug}/export/markdown (ZIP)
Labels GET/POST /ponds/{slug}/labels, PATCH/DELETE /ponds/{slug}/labels/{id}, PUT/DELETE /ponds/{slug}/pages/{pageSlug}/labels/{id}
Comments GET/POST /ponds/{slug}/pages/{pageSlug}/comments, POST/DELETE …/comments/{id}/resolve

Deliberately not in v1 (stage 2): attachment upload, version endpoints, webhooks.

Connect Claude Code / MCP clients

The instance ships its own MCP endpoint (Streamable HTTP) at /api/mcp — no extra process. It has its own switches, independent of the REST API: the instance switch under Admin → Settings → Public API ("Enable the built-in MCP endpoint") and a per-pond opt-in in the pond settings. Authentication uses the same personal access tokens.

Claude Code (or any Streamable-HTTP client):

claude mcp add --transport http dorfteich https://your-instance.example/api/mcp \
  --header "Authorization: Bearer dt_pat_..."

Stdio-only clients bridge with mcp-remote:

{
  "mcpServers": {
    "dorfteich": {
      "command": "npx",
      "args": [
        "mcp-remote",
        "https://your-instance.example/api/mcp",
        "--header",
        "Authorization: Bearer dt_pat_..."
      ]
    }
  }
}

Tools: list_ponds, list_pages, read_page, search, create_page, update_page (replace semantics, collab-safe like the REST PATCH), add_comment, list_labels, set_page_labels, export_pond. A tool call acts as the token's user; write tools need the write scope, and ponds without the MCP opt-in stay invisible (404 semantics). The endpoint is stateless — no sessions to manage, safe behind load balancers.

Security notes

  • Bearer tokens only — no cookies are involved, so there is no CSRF surface; browser sessions cannot call the public API and tokens cannot manage tokens.
  • Token creation and revocation are audit-logged, as is every write through the API (action api.write, with the token attributed).
  • Treat a token like a password. Revoke it under Settings → API tokens the moment it may have leaked.