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

8.3 KiB
Raw Blame History

name, type, slice, last_verified
name type slice last_verified
MCP tools (list, get, search, list_tasks) architecture-map-page 04-llm-formatting-mcp 2026/05/09

MCP tools

Where you are: Architecture mapLLM, Formatting, MCP → 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:
    {
      "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:
    {
      "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:

    {
      "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:
    {
      "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