diff --git a/docs/2026-03-23-fable-mcp-design.md b/docs/2026-03-23-fable-mcp-design.md index 9f05148..aa1a548 100644 --- a/docs/2026-03-23-fable-mcp-design.md +++ b/docs/2026-03-23-fable-mcp-design.md @@ -28,6 +28,8 @@ All existing Fable routes use session-based authentication (`session["user_id"]` ### Database New table `api_keys` via migration `0027_add_api_keys.py`: +- `down_revision = "0026"` (the briefing tables migration) +- `revision = "0027"` | Column | Type | Notes | |--------|------|-------| @@ -54,9 +56,11 @@ Full keys are generated as `fmcp_<32 random url-safe chars>`, returned once at c Session auth is untouched — no regression risk for the web UI. +**Admin routes and API keys:** Routes decorated with `admin_required` check `user.role == "admin"` after auth resolves. API keys authenticate as the key owner — so a write-scoped key for a non-admin user will fail admin-protected routes with 403 (role check, not scope check). This is intentional: API keys cannot elevate privilege beyond the user's role. + ### Routes (`/api/api-keys`) -New blueprint `api_keys_bp`: +New blueprint `api_keys_bp`, registered in `app.py`: - `GET /api/api-keys` — list caller's keys (prefix, name, scope, last_used_at, created_at — never the hash or full key) - `POST /api/api-keys` — create key; body: `{name, scope}`. Returns full key in response **once only** @@ -73,9 +77,14 @@ New "API Keys" tab in `SettingsView.vue` (added to `VALID_TABS`): ### New Search Endpoint -`GET /api/search?q=&type=note|task|all&limit=N` +`GET /api/search?q=&content_type=note|task|all&limit=N` -Calls the existing `semantic_search_notes()` service (already in `services/embeddings.py`). Returns: +Calls the existing `semantic_search_notes()` service (already in `services/embeddings.py`). The `content_type` parameter maps to the service's `is_task` argument: +- `content_type=note` → `is_task=False` +- `content_type=task` → `is_task=True` +- `content_type=all` (default) → `is_task=None` + +Returns: ```json { @@ -88,6 +97,37 @@ Calls the existing `semantic_search_notes()` service (already in `services/embed This endpoint is needed by the MCP's `search.py` tool module. It reuses existing infrastructure with no new ML work. +### Conversation Type: `"mcp"` + +A new conversation type `"mcp"` is added alongside `"chat"` and `"briefing"`. This requires changes in two places in the main app: + +**`services/chat.py`** — `create_conversation(user_id, title, model)` gains an optional `conversation_type: str = "chat"` parameter, passed through to the `Conversation` constructor. + +**`routes/chat.py`** — `POST /api/chat/conversations` accepts an optional `conversation_type` body field (defaults to `"chat"`), passed to `create_conversation`. `GET /api/chat/conversations` continues to default-filter to `conversation_type="chat"` (excluding `"mcp"` and `"briefing"`); pass `?type=mcp` to retrieve MCP conversations. + +**Retention:** `cleanup_old_conversations` in `routes/chat.py` currently deletes conversations regardless of type. It must be updated to exclude `conversation_type="mcp"` from the sweep, so MCP audit-trail conversations are not automatically pruned. + +MCP conversations: +- Are created with a caller-supplied name (e.g., `"MCP Session 2026-03-23"`) or auto-named +- Are excluded from the default chat list +- Are accessible via `GET /api/chat/conversations?type=mcp` for inspection +- Appear in the Fable UI if explicitly navigated to, providing an audit trail of MCP-driven interactions + +### Chat SSE Wire Format + +The MCP's `chat.py` tool must consume the existing SSE stream. The relevant endpoints and event schema: + +- **Create conversation:** `POST /api/chat/conversations` → `{id, title, ...}` +- **Post message + start generation:** `POST /api/chat/conversations/:id/messages` → `{message_id, ...}`; then connect to: +- **Stream:** `GET /api/chat/conversations/:id/stream` (SSE) + +SSE event format (each line is `data: `): +- `{"type": "token", "content": "..."}` — streaming token +- `{"type": "tool_call", "name": "...", "result": {...}}` — tool fired +- `{"type": "done", "content": "...", "tools_used": [...]}` — generation complete; this is the termination event + +The MCP tool reads tokens until it receives `type: "done"`, then returns `response` (full content) and `tools_used` (list of tool names). + --- ## Sub-project 2: Fable MCP Server @@ -109,12 +149,13 @@ fable-mcp/ client.py # FableClient: async httpx wrapper, env var config, FableAPIError tools/ __init__.py - notes.py # list_notes, get_note, create_note, update_note, delete_note, search_notes + notes.py # list_notes, get_note, create_note, update_note, delete_note tasks.py # list_tasks, get_task, create_task, update_task, delete_task, # patch_task_status, add_task_log projects.py # list_projects, get_project, create_project, update_project, # delete_project, get_project_summary - milestones.py # list_milestones, get_milestone, create_milestone, update_milestone + milestones.py # list_milestones, get_milestone, create_milestone, + # update_milestone, delete_milestone search.py # semantic_search — calls GET /api/search chat.py # send_message — creates/continues conversation, returns response ``` @@ -172,17 +213,18 @@ Each module imports `_client` from `client.py` and defines tools as `async def` - `get_milestone(project_id, milestone_id)` - `create_milestone(project_id, title, description, order_index)` - `update_milestone(project_id, milestone_id, title, description, status, order_index)` +- `delete_milestone(project_id, milestone_id)` **`search.py`** tools: -- `semantic_search(query, type, limit)` — calls `GET /api/search`, returns ranked results with similarity scores +- `semantic_search(query, content_type, limit)` — `content_type` is `"note"`, `"task"`, or `"all"` (avoids shadowing Python's `type` builtin). Calls `GET /api/search`, returns ranked results with similarity scores. **`chat.py`** tools: - `send_message(message, conversation_name)`: - - Looks up or creates a Fable conversation with the given name and type `"mcp"` + - Looks up or creates a Fable conversation with the given name and `conversation_type="mcp"` - Posts the message via `POST /api/chat/conversations/:id/messages` - - Polls the SSE stream until generation completes + - Consumes SSE stream from `GET /api/chat/conversations/:id/stream` until `type: "done"` - Returns: `{response: str, tools_used: [str], conversation_id: int}` - - MCP conversations use type `"mcp"` and are excluded from the normal chat UI list + - MCP conversations are excluded from the normal chat UI list ### Error Handling @@ -212,16 +254,6 @@ Claude Code spawns the process over stdio automatically. No Docker, no daemon. --- -## Conversation Type: `"mcp"` - -A new conversation type `"mcp"` is added alongside `"chat"` and `"briefing"`. Conversations of this type: -- Are created with a caller-supplied name (e.g., `"MCP Session 2026-03-23"`) or auto-named -- Are excluded from `GET /api/chat/conversations` (the main chat list) by default -- Are accessible via `GET /api/chat/conversations?type=mcp` if inspection is needed -- Appear in the Fable UI if explicitly navigated to, providing an audit trail of MCP-driven interactions - ---- - ## Build & Repo Plan 1. Implement and test within `fabledassistant/fable-mcp/`