feat(phase 3a mcp): MCP surface so mai/otto/Claude can read+write projax

mcp package (new): minimal JSON-RPC 2.0 + MCP-protocol server, tools
delegate to *store.Store (no business-logic duplication).

- handler.go: handleRPC routes initialize / tools/list / tools/call /
  ping / notifications/initialized; Bearer-token middleware; results
  flow through the standard MCP content[].text envelope; tool errors
  surface as isError: true (transport errors stay JSON-RPC errors).
- tools.go: 10 tools — list_items / get_item / create_item /
  update_item / delete_item / list_links / add_link / remove_link /
  search / tree. Multi-parent in/out — parent_paths[] string array,
  resolved per call. itemView/linkView keep the wire shape snake_case
  and stable.
- mcp_test.go + tools_test.go: protocol primitives (no DB) plus a
  full create → get → search → delete round-trip skipping cleanly
  when the DB env is absent. Multi-parent assertion discovers the
  test pair from the live DB rather than hard-coding a row.

store extensions:
- ListByFilters(SearchFilters) with parent_path/tags/management/kind/
  status/q/has_repo/has_caldav predicates.
- Search(q, limit) ranked across title/slug/aliases/content_md.
- GetByPathOrSlug for callers that don't know the full path.
- SoftDeleteCascade refuses on live descendants unless cascade=true.

web:
- New optional Server.MCP http.Handler. main.go mounts an mcp.Server
  when PROJAX_MCP_TOKEN is set; /mcp/* gets a StripPrefix and bypasses
  the Supabase-cookie auth middleware (its own Bearer auth applies).
- Off cleanly when the token is unset.

ops:
- ~/.claude/mcp/projax.sh stdio→HTTP bridge (NDJSON in, NDJSON out,
  Bearer header).
- .mcp.json adds an http-transport entry for clients that speak
  HTTP+MCP natively.
- deploy/dokploy.yaml advertises PROJAX_MCP_TOKEN as a secret.
- docs/design.md §7 added: tool list, multi-parent semantics, env
  contract, transport + bridge.
This commit is contained in:
mAi
2026-05-15 17:59:03 +02:00
parent 75a67c6a8b
commit dc50823860
11 changed files with 1666 additions and 2 deletions

View File

@@ -264,6 +264,62 @@ m's Gitea instance lives at `mgit.msbls.de` (token auth, automation account `mAi
Env contract: `GITEA_URL` (e.g. `https://mgit.msbls.de`, no `/api/v1` suffix), `GITEA_TOKEN`. Both live in Dokploy secrets; `GITEA_URL` unset → integration off cleanly (Issues section just doesn't render). `GITEA_URL` set but `GITEA_TOKEN` missing → refuse to start.
## 7. MCP surface (Phase 3a)
projax exposes its data + writes through an MCP server mounted on the same binary at `/mcp/rpc`. Mirrors the conventions of `mcp__mai__*` and `mcp__mai-memory__*` — one tool per coherent operation, snake_case names, structured JSON results carried inside the standard MCP `content[].text` envelope.
### Tools
| name | summary | key inputs |
|-------------------|---------|------------|
| `list_items` | List items with filters | `parent_path`, `tags[]`, `management[]`, `kind[]`, `status`, `q`, `has_repo`, `has_caldav`, `limit` |
| `get_item` | Fetch one item by id or path | `id` xor `path`, `include_links` (default true) |
| `create_item` | Create a new item | `slug`, `title`, `parent_paths[]`, `kind[]`, `tags[]`, `management[]`, `content_md`, `status`, `metadata` |
| `update_item` | Partial update of an existing item | `id` xor `path`, any subset of editable fields |
| `delete_item` | Soft-delete; refuses on live descendants unless `cascade=true` | `id` xor `path`, `cascade` |
| `list_links` | List item_links attached to an item | `id` xor `path`, optional `ref_type` |
| `add_link` | Add an external item_link | `ref_type`, `ref_id`, `rel`, `note`, `metadata` |
| `remove_link` | Delete an item_link by id | `link_id` |
| `search` | Ranked substring search across title/slug/aliases/content_md | `query`, `limit` |
| `tree` | Nested tree (multi-parent items appear under each branch) | `root_path`, `depth` |
### Output shape
All tools return a JSON object inside a single MCP text-content block. `list_items`, `list_links`, `search`, `tree` return `{count|roots, items|links|tree}`. `get_item` and write tools return a single `itemView` / `linkView` with snake_case fields matching `projax.items_unified`'s columns.
### Multi-parent semantics
- `list_items` with `parent_path='work'` matches any item whose `paths[]` contains a path equal to `work` or beginning with `work.` — multi-parent items surface from any ancestor.
- `get_item` resolves either by uuid or by any path the row publishes; `dev.paliad` and `work.paliad` return the same row.
- `create_item` accepts `parent_paths` as a string array: `[]` for a root, `['work']` for single-parent, `['work', 'dev']` for multi.
- `update_item` with a non-nil `parent_paths` *replaces* the full parent list; pass the current list plus the new one to add a parent.
- `tree` honours multi-parent — the same uuid appears under each branch with its inherited path as the node's `path` field.
### Transport + auth
- HTTP+JSON-RPC 2.0 over `POST /mcp/rpc` (no SSE needed at v1 — every tool returns synchronously).
- Bearer auth via `Authorization: Bearer <PROJAX_MCP_TOKEN>`. `/mcp/*` paths are exempt from the cookie auth middleware so API callers don't need a Supabase session.
- A GET on `/mcp/rpc` returns a small descriptor `{server, version, protocolVersion, tools[], authRequired}` for ops smoke-testing.
### Bridge for stdio MCP clients
`~/.claude/mcp/projax.sh` is a tiny bash bridge: reads NDJSON JSON-RPC frames from stdin, POSTs each to `${PROJAX_MCP_URL}/rpc` with the Bearer header, writes the response back to stdout. The repo-root `.mcp.json` exposes both wirings:
- An `http` server entry for clients that speak HTTP+MCP natively.
- A `command` server entry (referenced separately under `~/.claude/mcp/projax.sh`) for stdio-only clients.
Neither encodes a token; both interpolate `${PROJAX_MCP_TOKEN}` at session start.
### Env contract
- `PROJAX_MCP_TOKEN` — 32-char Bearer secret. Unset → `/mcp/*` returns 404 (off cleanly, the web UI keeps working). Set → routes mount, every request requires the matching Bearer.
Out of scope (parked):
- Server-pushed notifications / SSE — phase 3b.
- Bulk import/export tools — phase 3b.
- Otto-PWA integration that consumes this surface — separate worker.
## 8. Open questions (post-PRD)
- **Path-trigger correctness** under cycle attempts: enforce acyclicity via check in trigger.