Files
Jake 26c7307607
Some checks failed
check / cargo check (macos-latest) (push) Has been cancelled
check / cargo check (ubuntu-22.04) (push) Has been cancelled
check / cargo check (windows-latest) (push) Has been cancelled
check / svelte build + lint (push) Has been cancelled
agent: lumotia-rebrand — docs, scripts, root config, residuals
Phase 9 of the rebrand cascade. Sweep covers everything the Phase 8
frontend pass deliberately skipped: docs/, root markdown, scripts,
Cargo.toml descriptions, code comments that survived earlier
word-boundary sed, plus a handful of identifiers caught on the final
verify pass.

transcription-app changes:
- README.md, HANDOVER.md, KNOWN-ISSUES.md, run.sh — magnotia/Magnotia
  -> lumotia/Lumotia.
- docs/ — sweep across all subdirs except docs/handovers/ (preserved
  as immutable audit trail). Includes architecture-map references
  to magnotia_core::*, magnotia_storage::*, etc. now pointing at
  lumotia_*; dev-setup.md tracing output examples (lumotia_startup
  target); brief/ + superpowers/ + issues/ + whisper-ecosystem/ +
  audit/.
- Cargo.toml descriptions on 9 crates (core, audio, cloud-providers,
  hotkey, llm, mcp, plus referenced others).
- crates/core/src/{error,hardware,recommendation,paths}.rs +
  crates/audio/src/wav.rs + crates/llm/src/model_manager.rs +
  crates/cloud-providers/src/keystore.rs + crates/mcp/src/lib.rs —
  doc comments and a model-manager user-agent string.
- Caught on final pass: BroadcastChannel("magnotia_task_sync") -> ...
  ("lumotia_task_sync"); magnotia_locale i18n localStorage key
  renamed + migration shim added; CSS keyframe names
  magnotiaPulse / magnotiaBar / magnotiaFade renamed in the design-
  system kit; magnotia_viewer_item / magnotia_viewer_mode handoff
  keys renamed in HistoryPage + viewer/+page.svelte; src/assets/
  wordmark.svg text.
- src-tauri/src/lib.rs comment cleanup ("magnotia era" was sed'd
  to "lumotia era" earlier — restored).

Preserved (intentional):
- crates/core/src/paths.rs — keeps "magnotia" / "Magnotia" / ".magnotia"
  legacy detection strings in legacy_and_target_paths() so the
  migration shim can still find user data from the magnotia era.
- src/lib/stores/{page,focusTimer}.svelte.ts + src/lib/i18n/index.ts
  — migration call sites reference the legacy magnotia keys
  deliberately.
- docs/handovers/ — historical audit trail.

cargo build --workspace passes. npm run check: 0 errors / 0 warnings
(3958 files). cargo test --workspace: 339 pass / 0 fail.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 12:38:03 +01:00

157 lines
8.2 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
name: MCP tools (list, get, search, list_tasks)
type: architecture-map-page
slice: 04-llm-formatting-mcp
last_verified: 2026/05/09
---
# MCP tools
> **Where you are:** [Architecture map](../README.md) → [LLM, Formatting, MCP](README.md) → MCP tools
**Plain English summary.** Four read-only tools surface Lumotia's transcripts and tasks to MCP clients. Each tool is a thin SQLite query over a `sqlx` pool with the result reshaped into the MCP "text content" envelope. Every tool path forbids writes at the connection level.
## At a glance
- Crate: `lumotia-mcp`
- Path: `crates/mcp/src/lib.rs:103-321` (registration and four tool functions)
- LOC: 218 across the four tool functions and the `tools_list_result`
- Public surface: tools are exposed via `handle_message`'s `tools/call` branch. No tool function is `pub`.
- External deps that matter: `lumotia_storage::{list_transcripts, get_transcript, search_transcripts, list_tasks}`. All four return typed `Vec<TranscriptRow>` / `Option<TranscriptRow>` / `Vec<TaskRow>` from `lumotia-storage` (slice 5).
- Tauri command that calls this: n/a — the tools are remote-callable from any MCP client. The Tauri app uses `lumotia_storage` directly without going through MCP.
## What's in here
### `list_transcripts` (`crates/mcp/src/lib.rs:188`)
Returns a paginated list of recent transcripts, most recent first.
- **Tool schema:** `{ limit?: integer (1200, default 20) }`.
- **Argument handling:** `args.is_null()` short-circuits to `Args::default()` so a client that sends `tools/call` without an `arguments` field gets defaults instead of -32602. This is the regression from review-of-review (`a5bc45e fix(cr-2026-04-22): list_transcripts accepts omitted arguments`). A malformed shape (e.g. `{"limit": "twenty"}`) still returns -32602 (`8400128 fix(cr-2026-04-22): list_transcripts tool returns -32602 on malformed params`).
- **Limit clamp:** `args.limit.unwrap_or(20).clamp(1, 200)`.
- **DB call:** `lumotia_storage::list_transcripts(pool, limit)`.
- **Per-row JSON:**
```json
{
"id": "...",
"title": "...",
"createdAt": "...",
"source": "...",
"duration": <seconds>,
"starred": <bool>,
"language": "...",
"preview": "<first 240 chars + …>"
}
```
- **Output envelope:** `{ "content": [{ "type": "text", "text": "<pretty-printed JSON array>" }] }`.
### `get_transcript` (`crates/mcp/src/lib.rs:234`)
Returns the full text and metadata of a single transcript.
- **Tool schema:** `{ id: string (required) }`. UUID from `list_transcripts` or `search_transcripts`.
- **DB call:** `lumotia_storage::get_transcript(pool, &args.id)`.
- **Not-found:** returns -32000 "Transcript {id} not found". Reserved server-error range; chosen because the client supplied a valid ID shape but no row matches.
- **Per-row JSON:**
```json
{
"id": "...",
"title": "...",
"text": "<full transcript>",
"createdAt": "...",
"source": "...",
"duration": <seconds>,
"engine": "<whisper | parakeet | ...>",
"modelId": "<asr model identifier>",
"language": "...",
"starred": <bool>,
"manualTags": "<...>",
"template": "<...>"
}
```
Note: `manualTags` and `template` are pass-through strings from the DB row; their internal shape is owned by slice 5.
### `search_transcripts` (`crates/mcp/src/lib.rs:265`)
Full-text search across all transcripts.
- **Tool schema:** `{ query: string (required), limit?: integer (1100, default 20) }`. The description note "FTS5 syntax supported" is exposed to the MCP client so it can advise the user (or LLM) on phrase queries.
- **Limit clamp:** `args.limit.unwrap_or(20).clamp(1, 100)`. Tighter than `list_transcripts` because search results are typically narrower.
- **DB call:** `lumotia_storage::search_transcripts(pool, &args.query, limit)`. Slice 5 owns the SQLite FTS5 binding.
- **Per-row JSON:**
```json
{
"id": "...",
"title": "...",
"createdAt": "...",
"preview": "<first 240 chars + …>",
"source": "..."
}
```
Search results omit `duration`, `starred`, `language` from the list-summary shape — they are not load-bearing for "did this transcript match my query?" and the MCP client should call `get_transcript` for a matched ID anyway.
### `list_tasks` (`crates/mcp/src/lib.rs:298`)
Returns every task (open and completed). No paging arguments — the working assumption is that task lists stay small enough to return fully.
- **Tool schema:** `{}`.
- **DB call:** `lumotia_storage::list_tasks(pool)`.
- **Per-row JSON:**
```json
{
"id": "...",
"text": "...",
"bucket": "<inbox | today | week | someday | done>",
"done": <bool>,
"doneAt": "<RFC3339 timestamp or null>",
"createdAt": "...",
"parentTaskId": "<UUID of parent if subtask, else null>"
}
```
### Helpers
- `text_content(text: String)` (`crates/mcp/src/lib.rs:323`) — wraps a string in the MCP `{ content: [{ type: "text", text }] }` envelope.
- `preview(text: &str, limit: usize)` (`crates/mcp/src/lib.rs:329`) — char-aware truncation with a trailing ellipsis. Uses `chars().count()` and `chars().take(limit)` so multi-byte sequences are not split. Non-truncating short input is just returned trimmed.
- `error(code, message)` (`crates/mcp/src/lib.rs:339`) — `JsonRpcError` builder with `data: None`.
- `error_response(id, code, message)` (`crates/mcp/src/lib.rs:347`) — wraps `error` in a full `JsonRpcResponse` with no result.
## Data flow
```
client tools/call request
→ call_tool: deserialise { name, arguments } (or -32602 invalid params)
→ match name:
list_transcripts → list_transcripts_tool(pool, args)
get_transcript → get_transcript_tool(pool, args)
search_transcripts → search_transcripts_tool(pool, args)
list_tasks → list_tasks_tool(pool)
other → -32602 "Unknown tool"
→ each tool:
- deserialise its own args struct (or -32602 invalid arguments)
- clamp / sanitise limits
- call lumotia_storage::* (or -32603 DB error)
- shape rows into JSON Value
- serde_json::to_string_pretty
- wrap in text_content envelope
→ returns Result<Value, JsonRpcError>
→ bubbles back to handle_message → JsonRpcResponse
```
## Watch-outs
- **All four tools serialise rows server-side as pretty-printed JSON inside a text payload.** This is the MCP `text` content convention; clients (or the LLM behind them) get a string they have to parse again. The double-serialise is part of the protocol — do not "optimise" by emitting structured content unless every consuming MCP client supports it.
- **Read-only enforced at the connection.** Even if a future tool function calls a write-shaped storage helper by mistake, SQLite rejects. The contract is doubly belt-and-braces: the API surface is a curated subset of `lumotia_storage`, and the connection is `mode=ro`.
- **`get_transcript` not-found is -32000, not -32601 or -32602.** -32000 is the JSON-RPC reserved server-error range. The choice is deliberate: the client's request was well-formed, the resource just does not exist. -32602 would be misleading (the params were valid in shape).
- **`search_transcripts` exposes raw FTS5 syntax.** A malformed FTS5 query (e.g. unbalanced parens) bubbles up from `lumotia_storage::search_transcripts` as a `-32603 DB error`. The error message includes the underlying SQLite error text, which is informative but exposes implementation detail. Acceptable for a local-first read-only server; a remote-facing one might want to sanitise.
- **`list_tasks` returns everything.** No pagination, no limit. A user with thousands of tasks would get a megabyte of JSON. The working assumption is that task counts stay in the low hundreds; if that ever stops being true, add a `limit` argument matching `list_transcripts`'s shape.
- **`preview` is char-count-bounded, not byte-count-bounded.** A 240-emoji preview takes more bytes than a 240-ASCII preview but the count is the same. This is the desired behaviour for "first 240 visible characters".
- **`engine` and `modelId` fields in `get_transcript` are pass-through.** A transcript created by an engine no longer in the registry would surface a stale identifier; the MCP server does not normalise.
## See also
- [MCP server entry and stdio protocol](mcp-server.md)
- Slice 5 (forthcoming) — schema and storage accessors
- [Slice README](README.md)