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:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user