Add MCP server, note mutation endpoint, and updated_at tracking (v3.0.0)

New MCP server (mcp/) exposes kb operations as native MCP tools over
Streamable HTTP with Bearer token auth. Supports collections via tag
conventions, chunked file uploads, and agent-side search patterns.

Engine gains PATCH /api/v1/notes/{id} for in-place note updates with
transactional re-chunk/re-embed, and updated_at column on documents.

Go client adds updatenote command and Patch HTTP method.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-02 21:34:55 +01:00
parent adeba21712
commit e7136a4a20
32 changed files with 1679 additions and 8 deletions
@@ -0,0 +1,205 @@
# MCP Server
## Purpose
The MCP server provides a Model Context Protocol interface to the kb engine, exposing knowledge base operations as native MCP tools over Streamable HTTP transport. It runs as a separate Docker container alongside the engine, translating MCP tool calls into engine HTTP API calls.
## Requirements
### Requirement: MCP server transport and deployment
The MCP server SHALL expose tools via Streamable HTTP transport. It SHALL run as a Docker container, configured to connect to the kb engine's HTTP API. It SHALL read `KB_ENGINE_URL` and `KB_API_KEY` from environment variables to connect to the engine.
#### Scenario: MCP server starts and connects to engine
- **WHEN** the MCP server container starts with `KB_ENGINE_URL=http://engine:8000` and `KB_API_KEY=secret`
- **THEN** it SHALL begin accepting MCP connections over Streamable HTTP and use the configured URL and API key for all engine API calls
#### Scenario: Engine unreachable at startup
- **WHEN** the MCP server starts but cannot reach the engine at `KB_ENGINE_URL`
- **THEN** it SHALL start and accept connections, but tool calls SHALL return errors indicating the engine is unreachable
#### Scenario: Docker Compose deployment
- **WHEN** the MCP server is deployed via Docker Compose alongside the engine
- **THEN** it SHALL connect to the engine via the Docker network using the service name (e.g. `http://engine:8000`)
---
### Requirement: MCP server authentication
The MCP server SHALL require Bearer token authentication from calling agents via the `KB_MCP_API_KEY` environment variable. This is independent of the engine's `KB_API_KEY`.
#### Scenario: Valid MCP API key
- **WHEN** `KB_MCP_API_KEY` is set and a calling agent provides a matching Bearer token
- **THEN** the MCP server SHALL process the request normally
#### Scenario: Missing MCP API key when required
- **WHEN** `KB_MCP_API_KEY` is set and a calling agent connects without a Bearer token
- **THEN** the MCP server SHALL reject the connection with an authentication error
#### Scenario: Invalid MCP API key
- **WHEN** `KB_MCP_API_KEY` is set and a calling agent provides a non-matching Bearer token
- **THEN** the MCP server SHALL reject the connection with an authentication error
#### Scenario: MCP auth disabled
- **WHEN** `KB_MCP_API_KEY` is not set
- **THEN** the MCP server SHALL accept all connections without authentication
---
### Requirement: Search tool
The MCP server SHALL expose a `kb_search` tool that queries the knowledge base via the engine's search API.
#### Scenario: Basic search
- **WHEN** an agent calls `kb_search` with `{"query": "pension revaluation", "top": 5}`
- **THEN** the MCP server SHALL POST to the engine's `/api/v1/search` endpoint and return the results with chunk text, scores, document metadata, and tags
#### Scenario: Search with collection filter
- **WHEN** an agent calls `kb_search` with `{"query": "email preferences", "collection": "memory"}`
- **THEN** the MCP server SHALL add `collection:memory` to the tags filter and POST to the engine's search endpoint
#### Scenario: Search with tags and collection
- **WHEN** an agent calls `kb_search` with `{"query": "feedback", "tags": ["email"], "collection": "memory"}`
- **THEN** the MCP server SHALL combine the explicit tags with `collection:memory` in the tag filter
#### Scenario: Search results strip collection tags
- **WHEN** the engine returns search results containing tags `["collection:memory", "feedback", "email"]`
- **THEN** the MCP server SHALL strip `collection:*` tags from the `tags` array and add a separate `collection` field, returning `{"collection": "memory", "tags": ["feedback", "email"], ...}`
#### Scenario: Search with mode override
- **WHEN** an agent calls `kb_search` with `{"query": "error log", "fts_only": true}`
- **THEN** the MCP server SHALL pass `fts_only: true` to the engine search endpoint
---
### Requirement: Add note tool
The MCP server SHALL expose a `kb_addnote` tool that submits a text note to the engine for ingestion.
#### Scenario: Add a note with default collection
- **WHEN** an agent calls `kb_addnote` with `{"text": "User prefers concise responses"}`
- **THEN** the MCP server SHALL submit the note to the engine's `POST /api/v1/jobs` endpoint with the tag `collection:documents` and return the job ID
#### Scenario: Add a note to a specific collection
- **WHEN** an agent calls `kb_addnote` with `{"text": "User prefers concise responses", "collection": "memory", "tags": ["feedback"]}`
- **THEN** the MCP server SHALL submit the note with tags `["collection:memory", "feedback"]` to the engine
#### Scenario: Add a note to a collection replaces existing collection tag
- **WHEN** an agent calls `kb_addnote` with `{"text": "some note", "collection": "memory"}` and the note is ingested
- **THEN** the resulting document SHALL have exactly one `collection:*` tag: `collection:memory`
---
### Requirement: Chunked file upload tools
The MCP server SHALL expose a three-step chunked file upload pattern for transferring files from remote agents to the engine.
#### Scenario: Start an upload
- **WHEN** an agent calls `kb_upload_start` with `{"filename": "report.pdf", "total_size": 5242880, "tags": ["insurance"], "collection": "documents"}`
- **THEN** the MCP server SHALL create a staging entry, generate a UUID `upload_id`, and return `{"upload_id": "<uuid>"}`
#### Scenario: Upload a chunk
- **WHEN** an agent calls `kb_upload_chunk` with `{"upload_id": "<uuid>", "data": "<base64-encoded-data>", "chunk_index": 0}`
- **THEN** the MCP server SHALL decode the base64 data and write it to the staging area for the given upload
#### Scenario: Upload multiple chunks in sequence
- **WHEN** an agent calls `kb_upload_chunk` multiple times with sequential `chunk_index` values for the same `upload_id`
- **THEN** the MCP server SHALL store each chunk and track the sequence
#### Scenario: Finish an upload
- **WHEN** an agent calls `kb_upload_finish` with `{"upload_id": "<uuid>"}`
- **THEN** the MCP server SHALL reassemble the chunks in order, forward the complete file as a multipart upload to the engine's `POST /api/v1/jobs` endpoint with the tags from `kb_upload_start` (including `collection:<name>`), and return the job ID
#### Scenario: Upload with invalid upload_id
- **WHEN** an agent calls `kb_upload_chunk` or `kb_upload_finish` with an `upload_id` that does not exist
- **THEN** the MCP server SHALL return an error indicating the upload ID is not found
#### Scenario: Abandoned upload cleanup
- **WHEN** an agent starts an upload but does not call `kb_upload_finish` within 10 minutes
- **THEN** the MCP server SHALL clean up the staged chunks and remove the upload tracking entry
#### Scenario: MCP server restart during upload
- **WHEN** the MCP server container restarts while an upload is in progress
- **THEN** the in-progress upload SHALL be lost and the agent SHALL need to restart from `kb_upload_start`
---
### Requirement: Update note tool
The MCP server SHALL expose a `kb_update_note` tool that updates an existing note in place via the engine's note mutation endpoint.
#### Scenario: Update an existing note
- **WHEN** an agent calls `kb_update_note` with `{"document_id": 42, "text": "Updated preference: user prefers bullet points"}`
- **THEN** the MCP server SHALL send `PATCH /api/v1/notes/42` to the engine and return the updated document
#### Scenario: Update a non-existent document
- **WHEN** an agent calls `kb_update_note` with a `document_id` that does not exist
- **THEN** the MCP server SHALL return an error indicating the document was not found
#### Scenario: Update a non-note document
- **WHEN** an agent calls `kb_update_note` with a `document_id` that refers to a PDF
- **THEN** the MCP server SHALL return an error indicating that only notes can be updated
---
### Requirement: Get document tool
The MCP server SHALL expose a `kb_get` tool that retrieves document details from the engine.
#### Scenario: Get by document ID
- **WHEN** an agent calls `kb_get` with `{"document_id": 42}`
- **THEN** the MCP server SHALL fetch `GET /api/v1/documents/42` and return the document details with chunks
#### Scenario: Get by source path
- **WHEN** an agent calls `kb_get` with `{"source_path": "memory/feedback_testing.md"}`
- **THEN** the MCP server SHALL query the engine's documents endpoint filtered by source path and return matching documents
#### Scenario: Get results strip collection tags
- **WHEN** the engine returns document details with tags including `collection:memory`
- **THEN** the MCP server SHALL strip `collection:*` from tags and present a separate `collection` field
---
### Requirement: Status tool
The MCP server SHALL expose a `kb_status` tool that returns engine health and statistics.
#### Scenario: Get engine status
- **WHEN** an agent calls `kb_status` with no parameters
- **THEN** the MCP server SHALL fetch `GET /api/v1/status` and return engine version, model info, device info, document counts, and queue state
---
### Requirement: Jobs tool
The MCP server SHALL expose a `kb_jobs` tool that returns ingestion job status.
#### Scenario: List recent jobs
- **WHEN** an agent calls `kb_jobs` with no parameters
- **THEN** the MCP server SHALL fetch `GET /api/v1/jobs` and return the list of recent jobs
#### Scenario: Filter jobs by status
- **WHEN** an agent calls `kb_jobs` with `{"status": "failed"}`
- **THEN** the MCP server SHALL fetch `GET /api/v1/jobs?status=failed` and return matching jobs
---
### Requirement: Collection management via tags
The MCP server SHALL manage collections using tag conventions. The MCP server SHALL enforce exclusive collection membership — a document SHALL belong to exactly one collection.
#### Scenario: Default collection on addnote
- **WHEN** an agent calls `kb_addnote` without specifying a collection
- **THEN** the MCP server SHALL apply the tag `collection:documents`
#### Scenario: Explicit collection on addnote
- **WHEN** an agent calls `kb_addnote` with `{"collection": "memory"}`
- **THEN** the MCP server SHALL apply the tag `collection:memory`
#### Scenario: Exclusive collection enforcement
- **WHEN** a document already has the tag `collection:documents` and an operation changes its collection to `memory`
- **THEN** the MCP server SHALL first remove `collection:documents` via the engine's tag API, then add `collection:memory`
#### Scenario: Collection field in search results
- **WHEN** search results include documents with `collection:*` tags
- **THEN** the MCP server SHALL present the collection as a top-level `collection` field and exclude `collection:*` from the `tags` array