Files
Lumotia/docs/architecture-map/04-llm-formatting-mcp/mcp-tools.md
jars a1f3f3f134 docs: architecture map (initial 5-slice generation, 105 pages)
Five-slice navigable map of the entire codebase under
docs/architecture-map/. Each slice is a self-contained
breadcrumbed sub-tree:

  01-frontend (16)              Svelte/SvelteKit UI
  02-tauri-runtime (26)         src-tauri commands + lifecycle
  03-audio-transcription (16)   audio + transcription crates
  04-llm-formatting-mcp (19)    llm, ai-formatting, mcp, cloud
  05-core-storage-hotkey-build  core, storage, hotkey, workspace,
                          (26) CI, dev glue

Plus master README.md and data-flow-end-to-end.md tracing
audio bytes from microphone to FTS5 search to MCP read.

Generated by 5 parallel subagents on 2026/05/09 against
HEAD 3c47000. Each page has YAML frontmatter, file:line code
refs, sibling cross-links, plain-English summaries.

Aggregated debt surfaced (full lists in master README):
RB-08 macOS power assertion, schema head drift v14 vs v15,
VAD blocked on ort version conflict, streaming primitives
not wired into live.rs, no prompt versioning, MCP has no
auth, cloud-providers in-memory keystore, SettingsPage
2 484 LOC, commands/live.rs 1 737 LOC, dual theme system,
brand rename to Lumenote pending across the codebase.

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

157 lines
8.3 KiB
Markdown
Raw 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 Magnotia'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: `magnotia-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: `magnotia_storage::{list_transcripts, get_transcript, search_transcripts, list_tasks}`. All four return typed `Vec<TranscriptRow>` / `Option<TranscriptRow>` / `Vec<TaskRow>` from `magnotia-storage` (slice 5).
- Tauri command that calls this: n/a — the tools are remote-callable from any MCP client. The Tauri app uses `magnotia_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:** `magnotia_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:** `magnotia_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:** `magnotia_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:** `magnotia_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 magnotia_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 `magnotia_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 `magnotia_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)