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

8.2 KiB
Raw Permalink 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 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:
    {
      "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:
    {
      "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:

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