Built-in MCP endpoint (Streamable HTTP) on top of the public API #105

Closed
opened 2026-07-12 09:37:54 +02:00 by fable-5 · 1 comment
Collaborator

Context

AI frameworks (Claude Code and other MCP clients) should talk to a Dorfteich instance directly. With personal access tokens and the public API in place (#104), the instance exposes MCP itself — no extra process for users; stdio-only clients bridge via mcp-remote.

Scope

  • Transport: MCP Streamable-HTTP endpoint at /mcp, served by the api (official @modelcontextprotocol/sdk server, stateless mode). Auth via PAT bearer (same tokens as #104). Honors its own instance switch (mcp.enabled, default off) plus a per-pond opt-in (mcpEnabled pond setting) — switchable independently of the REST API.
  • Tools (thin wrappers over the public-API services, same permission gates and scopes):
    • list_ponds() / list_pages(pond)
    • read_page(pond, page) → Markdown + metadata
    • search(query, pond?, label?) → snippets + stable refs
    • create_page(pond, title, markdown)
    • update_page(pond, page, markdown?, title?) — replace semantics, like the REST PATCH (collab-safe content path)
    • add_comment(pond, page, text)
    • list_labels(pond) / set_page_labels(pond, page, labelIds)
    • export_pond(pond) → link to the ZIP export
  • Resources (stretch goal, may drop to stage 2): pages exposed as MCP resources (dorfteich://{pond}/{page}) for clients that prefer resource reads.
  • Tool descriptions in English; results carry stable ids/slugs so agents can chain calls. Errors map to MCP tool errors carrying the api error codes.
  • Docs: a docs/self-hosting section "Connect Claude Code / MCP clients" with the PAT walkthrough, the direct URL config, and the mcp-remote one-liner for stdio clients.

Acceptance criteria

  • Claude Code connects with URL + PAT and can search, read, create and update pages in an opted-in pond end-to-end
  • anonymous or wrong-scope tokens are rejected; non-opted-in ponds stay invisible through MCP (404 semantics)
  • the MCP instance/pond switches work independently of the REST switches
  • e2e: a scripted MCP client run in CI against the fresh stack (initialize, list tools, page round-trip)
  • write tools respect the write scope; read-only tokens can list/read/search only

Technical notes

  • Mount the SDK's Streamable-HTTP handler inside the Nest app (raw express route beside the versioned REST routes); keep it stateless (no session store) so multiple api replicas stay trivial.
  • Rate limits per token via the existing RateLimitService; tool invocations audit-logged like public-API writes.

Dependencies

Depends on #104 (personal access tokens + public API services).

Size: ~2 days


Conventions: English code/comments, clear human-readable code, no hard-coded UI strings (ADR 0012, add de and en), permission checks only via the shared guard (docs/architecture/permissions.md). Read the referenced ADRs before starting.

## Context AI frameworks (Claude Code and other MCP clients) should talk to a Dorfteich instance directly. With personal access tokens and the public API in place (#104), the instance exposes MCP itself — no extra process for users; stdio-only clients bridge via `mcp-remote`. ## Scope - **Transport**: MCP Streamable-HTTP endpoint at `/mcp`, served by the api (official `@modelcontextprotocol/sdk` server, stateless mode). Auth via PAT bearer (same tokens as #104). Honors its own instance switch (`mcp.enabled`, default off) plus a per-pond opt-in (`mcpEnabled` pond setting) — switchable independently of the REST API. - **Tools** (thin wrappers over the public-API services, same permission gates and scopes): - `list_ponds()` / `list_pages(pond)` - `read_page(pond, page)` → Markdown + metadata - `search(query, pond?, label?)` → snippets + stable refs - `create_page(pond, title, markdown)` - `update_page(pond, page, markdown?, title?)` — replace semantics, like the REST PATCH (collab-safe content path) - `add_comment(pond, page, text)` - `list_labels(pond)` / `set_page_labels(pond, page, labelIds)` - `export_pond(pond)` → link to the ZIP export - **Resources** (stretch goal, may drop to stage 2): pages exposed as MCP resources (`dorfteich://{pond}/{page}`) for clients that prefer resource reads. - Tool descriptions in English; results carry stable ids/slugs so agents can chain calls. Errors map to MCP tool errors carrying the api error codes. - **Docs**: a docs/self-hosting section "Connect Claude Code / MCP clients" with the PAT walkthrough, the direct URL config, and the `mcp-remote` one-liner for stdio clients. ## Acceptance criteria - [ ] Claude Code connects with URL + PAT and can search, read, create and update pages in an opted-in pond end-to-end - [ ] anonymous or wrong-scope tokens are rejected; non-opted-in ponds stay invisible through MCP (404 semantics) - [ ] the MCP instance/pond switches work independently of the REST switches - [ ] e2e: a scripted MCP client run in CI against the fresh stack (initialize, list tools, page round-trip) - [ ] write tools respect the `write` scope; read-only tokens can list/read/search only ## Technical notes - Mount the SDK's Streamable-HTTP handler inside the Nest app (raw express route beside the versioned REST routes); keep it stateless (no session store) so multiple api replicas stay trivial. - Rate limits per token via the existing `RateLimitService`; tool invocations audit-logged like public-API writes. ## Dependencies Depends on #104 (personal access tokens + public API services). **Size**: ~2 days --- *Conventions: English code/comments, clear human-readable code, no hard-coded UI strings (ADR 0012, add `de` **and** `en`), permission checks only via the shared guard (docs/architecture/permissions.md). Read the referenced ADRs before starting.*
fable-5 added this to the M11 — Public API & MCP milestone 2026-07-12 09:37:54 +02:00
fable-5 added the
docs
backend
labels 2026-07-12 09:37:54 +02:00
Author
Collaborator

Implemented in 04e21a0 (CI fully green, deployed to Test + Int).

What shipped

  • Endpoint /api/mcp — deliberately under the /api/ path instead of the issue's literal /mcp: every existing reverse proxy (stages, self-hosters, the bundled Caddyfile) already routes /api/* to the api, so the feature needs zero deployment changes anywhere.
  • Transport: official @modelcontextprotocol/sdk, Streamable HTTP, fully stateless — each POST builds a fresh server+transport pair (no session store; replicas stay trivial), GET/DELETE answer 405. Auth via PAT bearer (#104 tokens), per-token rate limit (429 + Retry-After).
  • Own switches, independent of REST (pinned by tests in both directions): instance mcp.enabled (admin settings, default off — off means 404, feature invisible) + pond setting mcpEnabled (pond-settings toggle, default off).
  • Tools (thin wrappers over the #104 services, same permission gates, writes audit-logged): 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). Errors carry the api error codes; results carry stable slugs/ids for chaining. MCP resources stay the documented stage-2 stretch goal.
  • Docs: "Connect Claude Code / MCP clients" section in docs/self-hosting/public-api.mdclaude mcp add --transport http … --header "Authorization: Bearer dt_pat_…" plus the mcp-remote bridge for stdio-only clients.

Implementation note: the SDK's typed registerTool helpers drove TypeScript's inference out of memory in a program this size — the server sits on the SDK's low-level Server API with a hand-written tool table (mcp-tools.ts, JSON Schema for tools/list + Zod validation of calls).

Verification

  • 8-test e2e pack driving the real MCP SDK client over Streamable HTTP against a listening api: initialize + tools/list, switch independence both ways (REST on ≠ MCP on), anonymous/garbage tokens 401, non-opted-in ponds invisible (404 semantics), full page roundtrip (create → read → update incl. restore-NOTIFY assertion → search → export link), labels/comments, read-scope tokens blocked from writes with scope_required.
  • Live check through the web proxy against a seeded stack (the exact client path Claude Code uses): tools listed, page created, read, replaced, found via search — passed.
  • Full api suite 61/61 test files green; lint/typecheck/i18n clean.

The "Claude Code connects" acceptance criterion is covered by the official SDK client over the same Streamable-HTTP transport Claude Code uses (claude mcp add --transport http); the config one-liner is documented and ready for a hands-on run against Test/Prod once tokens exist there.

Implemented in `04e21a0` (CI fully green, deployed to Test + Int). **What shipped** - **Endpoint `/api/mcp`** — deliberately under the `/api/` path instead of the issue's literal `/mcp`: every existing reverse proxy (stages, self-hosters, the bundled Caddyfile) already routes `/api/*` to the api, so the feature needs zero deployment changes anywhere. - **Transport**: official `@modelcontextprotocol/sdk`, Streamable HTTP, fully **stateless** — each POST builds a fresh server+transport pair (no session store; replicas stay trivial), GET/DELETE answer 405. Auth via PAT bearer (#104 tokens), per-token rate limit (429 + Retry-After). - **Own switches, independent of REST** (pinned by tests in both directions): instance `mcp.enabled` (admin settings, default off — off means 404, feature invisible) + pond setting `mcpEnabled` (pond-settings toggle, default off). - **Tools** (thin wrappers over the #104 services, same permission gates, writes audit-logged): `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). Errors carry the api error codes; results carry stable slugs/ids for chaining. MCP resources stay the documented stage-2 stretch goal. - **Docs**: "Connect Claude Code / MCP clients" section in `docs/self-hosting/public-api.md` — `claude mcp add --transport http … --header "Authorization: Bearer dt_pat_…"` plus the `mcp-remote` bridge for stdio-only clients. **Implementation note**: the SDK's typed `registerTool` helpers drove TypeScript's inference out of memory in a program this size — the server sits on the SDK's low-level `Server` API with a hand-written tool table (`mcp-tools.ts`, JSON Schema for tools/list + Zod validation of calls). **Verification** - 8-test e2e pack driving the **real MCP SDK client** over Streamable HTTP against a listening api: initialize + tools/list, switch independence both ways (REST on ≠ MCP on), anonymous/garbage tokens 401, non-opted-in ponds invisible (404 semantics), full page roundtrip (create → read → update incl. restore-NOTIFY assertion → search → export link), labels/comments, read-scope tokens blocked from writes with `scope_required`. - Live check through the web proxy against a seeded stack (the exact client path Claude Code uses): tools listed, page created, read, replaced, found via search — passed. - Full api suite 61/61 test files green; lint/typecheck/i18n clean. The "Claude Code connects" acceptance criterion is covered by the official SDK client over the same Streamable-HTTP transport Claude Code uses (`claude mcp add --transport http`); the config one-liner is documented and ready for a hands-on run against Test/Prod once tokens exist there.
Sign in to join this conversation.
No project
No Assignees
1 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: stwaidele/dorfteich#105
No description provided.