Files
Lumotia/docs/architecture-map/04-llm-formatting-mcp/mcp-server.md
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

9.7 KiB

name, type, slice, last_verified
name type slice last_verified
MCP server entry and stdio protocol architecture-map-page 04-llm-formatting-mcp 2026/05/09

MCP server entry and stdio protocol

Where you are: Architecture mapLLM, Formatting, MCP → MCP server

Plain English summary. lumotia-mcp is a standalone binary that speaks JSON-RPC 2.0 over stdio. It opens Lumotia's SQLite store read-only and exposes four tools to any MCP-capable client (Claude desktop, Cline, Goose, etc). One stdin line per request, one stdout line per response, stderr for logs. No auth — stdio access is the trust boundary.

At a glance

  • Crate: lumotia-mcp
  • Paths:
    • crates/mcp/src/main.rs — 53 LOC binary entry
    • crates/mcp/src/lib.rs — 531 LOC dispatcher and tool implementations
  • Public surface (from lib.rs):
    • pub const PROTOCOL_VERSION: &str = "2024-11-05" (:14)
    • pub const SERVER_NAME: &str = "lumotia-mcp" (:15)
    • pub const SERVER_VERSION: &str = env!("CARGO_PKG_VERSION") (:16)
    • pub struct JsonRpcRequest (:18)
    • pub struct JsonRpcResponse (:28)
    • pub struct JsonRpcError (:38)
    • pub async fn handle_message(pool: &SqlitePool, raw: Value) -> Option<JsonRpcResponse> (:49)
    • pub fn parse_error_response(detail: &str) -> JsonRpcResponse (:362)
  • External deps that matter: sqlx 0.8 with runtime-tokio and sqlite features (no migrations from this binary), serde_json, tokio (current_thread flavor), anyhow, lumotia-storage for the read-only init and the data accessors.
  • Tauri command that calls this: n/a — the binary is invoked by external MCP clients, never by Tauri.

What's in here

main.rs — stdio loop (crates/mcp/src/main.rs:1)

#[tokio::main(flavor = "current_thread")]. Single-thread Tokio runtime — no parallelism inside the binary; one client at a time over a pipe.

Steps:

  1. Resolve database path. lumotia_storage::database_path() — slice 5 owns the path computation. Logged to stderr.
  2. Open read-only. lumotia_storage::init_readonly(&db_path).await?. The init call sets the SQLite connection's URI to the read-only mode at the connection level, so this binary cannot write regardless of which tools the dispatcher exposes. Migrations are deliberately skipped — only the main app owns the schema.
  3. Stdin loop. Buffered line reader. For each non-empty line:
    • Parse as serde_json::Value.
    • On parse error: log to stderr, build a parse_error_response (code -32700, id null), write to stdout. Previously this branch logged-and-continued, dropping the response, which left clients in silence; the 2026-04-22 review flagged it as a MAJOR (d25b095 fix(cr-2026-04-22): MCP stdio replies with parse-error on malformed JSON).
    • On parse success: dispatch via handle_message. If None (notification — no id), continue without writing. Otherwise serialise the response and write a single line.
  4. Flush after every response. Pipe-buffering would otherwise stall the client.

lib.rs — dispatcher

handle_message (crates/mcp/src/lib.rs:49)

The single dispatch function. Takes the parsed JSON and the SQLite pool.

Steps:

  1. Deserialise into JsonRpcRequest. Shape mismatch (e.g. wrong types) becomes a parse error response with id: Value::Null and code -32700.
  2. Notification check. A request with no id field is a JSON-RPC notification. Return None. MCP clients send notifications/initialized after the initialise handshake; this is the only notification we expect, but the protocol allows others.
  3. Method dispatch. A match on request.method.as_str():
    • "initialize"initialize_result() (:89).
    • "tools/list"tools_list_result() (:103).
    • "tools/call"call_tool(pool, request.params).await.
    • "ping"Ok(json!({})). MCP clients sometimes send a ping; we reply with an empty object.
    • any other → Err(error(-32601, "Method not found: {other}")).
  4. Response wrapping. Ok(result) and Err(err) both produce a JsonRpcResponse with jsonrpc: "2.0" and the request's id.

initialize_result (crates/mcp/src/lib.rs:89)

Returns the protocol-mandated handshake:

{
  "protocolVersion": "2024-11-05",
  "capabilities": { "tools": {} },
  "serverInfo": { "name": "lumotia-mcp", "version": "0.1.0" },
  "instructions": "Read-only access to Lumotia's local transcript history and task list. All data stays on the user's machine."
}

The instructions field is what an MCP host shows the user as "what does this server do".

tools_list_result (crates/mcp/src/lib.rs:103)

Returns the four tool schemas. See mcp-tools.md for the full tool detail. Tool names are stable: list_transcripts, get_transcript, search_transcripts, list_tasks. Each tool advertises a JSON Schema for its inputSchema.

call_tool (crates/mcp/src/lib.rs:168)

Inner-deserialise the params shape { name: String, arguments: Value }. Failure → -32602 Invalid params. Then a match on name:

  • "list_transcripts"list_transcripts_tool(pool, arguments)
  • "get_transcript"get_transcript_tool(pool, arguments)
  • "search_transcripts"search_transcripts_tool(pool, arguments)
  • "list_tasks"list_tasks_tool(pool)
  • otherwise → -32602 "Unknown tool: ..."

Each tool function returns Result<Value, JsonRpcError> shaped as { content: [{ type: "text", text: <pretty-printed JSON string> }] } per MCP's standard tool-output convention.

Error codes used

Code Where Meaning
-32700 parse_error_response JSON parse failure
-32601 handle_message other branch Method not found
-32602 call_tool invalid params, *_tool invalid arguments, unknown tool Invalid params
-32603 every *_tool DB error path Internal error (DB error)
-32000 get_transcript_tool not-found Server error reserved range; "Transcript {id} not found"

parse_error_response (crates/mcp/src/lib.rs:362)

Public helper called from main.rs's parse-error branch. Emits a JSON-RPC 2.0 Parse Error with code -32700 and id: Value::Null. The 2026-04-22 review fix.

Tests (crates/mcp/src/lib.rs:366)

  • initialize_returns_server_info (:370) — handshake works without DB.
  • notification_without_id_produces_no_response (:388) — notifications are silent.
  • tools_list_advertises_four_tools (:401) — tool name list is exact.
  • parse_error_response_has_jsonrpc_2_0_shape (:432) — the parse-error helper.
  • list_transcripts_accepts_omitted_arguments (:446) — regression for the review-of-review (a5bc45e): arguments omitted entirely (Value::Null) must not error.
  • list_transcripts_rejects_malformed_params_with_invalid_arguments (:476) — regression for the original review (8400128): a malformed arguments shape returns -32602, not silent default.
  • unknown_method_returns_method_not_found_error (:502) — -32601 for unknown methods.
  • preview_truncates_at_boundary and preview_keeps_short_text_intact (:517, :526) — helper coverage.

Data flow

external MCP client
  ↓ stdin (newline-delimited JSON-RPC 2.0)
lumotia-mcp main.rs loop
  → serde_json::from_str → Value
       (or parse_error_response on failure)
  → lumotia_mcp::handle_message(&pool, raw)
       → JsonRpcRequest deserialise (or -32700 on shape mismatch)
       → notification check (None)
       → method dispatch:
            initialize → server info
            tools/list → tool schemas
            tools/call → call_tool → list/get/search/list_tasks tool
            ping → {}
            other → -32601
       → response or None
  → serde_json::to_string + "\n" + flush
  ↓ stdout
external MCP client

The SQLite pool is opened once at startup and shared across the whole loop. init_readonly from lumotia-storage configures the connection with mode=ro, so a misbehaving tool implementation cannot write.

Watch-outs

  • No auth, no transport-level scoping. Anyone with stdio access to this binary has read access to every transcript and task in the user's local store. Lumotia's threat model treats stdio as a trust boundary; cloud / HTTP transport would need an auth layer. Tracked in the slice README.
  • Read-only at the connection level, not just at the tool layer. Even if a future contributor adds a write-shaped tool by mistake, the SQLite connection rejects it. Defense in depth.
  • current_thread Tokio runtime. No internal parallelism. A request that takes 5 seconds blocks the next request. Acceptable because (a) MCP is a single-client protocol over stdio and (b) every read tool is a quick SQLite query. Worth knowing if a future tool ever makes a network call (it should not).
  • Logs go to stderr deliberately. Stdout is the JSON-RPC channel; mixing logs in would corrupt the stream. Every log uses eprintln!; this is enforced by convention only — there is no tracing setup gating the writers.
  • Migrations are skipped. The binary's comment at crates/mcp/src/main.rs:18 makes this explicit. The main app is the single migration writer. If the main app has not been run yet (no DB exists), init_readonly will fail and the binary exits.
  • Notifications are dropped silently. The MCP spec sends notifications/initialized after the handshake. We accept it (no error) but do not act on it; the connection is already "ready" by the time we see it.
  • Empty stdin lines are skipped. A keepalive that emits a blank line does not produce a parse error.

See also