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>
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 map → LLM, Formatting, MCP → MCP server
Plain English summary. magnotia-mcp is a standalone binary that speaks JSON-RPC 2.0 over stdio. It opens Magnotia'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:
magnotia-mcp - Paths:
crates/mcp/src/main.rs— 53 LOC binary entrycrates/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 = "magnotia-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.8withruntime-tokioandsqlitefeatures (no migrations from this binary),serde_json,tokio(current_threadflavor),anyhow,magnotia-storagefor 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:
- Resolve database path.
magnotia_storage::database_path()— slice 5 owns the path computation. Logged to stderr. - Open read-only.
magnotia_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. - 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, idnull), 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. IfNone(notification — noid), continue without writing. Otherwise serialise the response and write a single line.
- Parse as
- 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:
- Deserialise into
JsonRpcRequest. Shape mismatch (e.g. wrong types) becomes a parse error response withid: Value::Nulland code -32700. - Notification check. A request with no
idfield is a JSON-RPC notification. ReturnNone. MCP clients sendnotifications/initializedafter the initialise handshake; this is the only notification we expect, but the protocol allows others. - Method dispatch. A
matchonrequest.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}")).
- Response wrapping.
Ok(result)andErr(err)both produce aJsonRpcResponsewithjsonrpc: "2.0"and the request'sid.
initialize_result (crates/mcp/src/lib.rs:89)
Returns the protocol-mandated handshake:
{
"protocolVersion": "2024-11-05",
"capabilities": { "tools": {} },
"serverInfo": { "name": "magnotia-mcp", "version": "0.1.0" },
"instructions": "Read-only access to Magnotia'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):argumentsomitted entirely (Value::Null) must not error.list_transcripts_rejects_malformed_params_with_invalid_arguments(:476) — regression for the original review (8400128): a malformedargumentsshape returns -32602, not silent default.unknown_method_returns_method_not_found_error(:502) — -32601 for unknown methods.preview_truncates_at_boundaryandpreview_keeps_short_text_intact(:517,:526) — helper coverage.
Data flow
external MCP client
↓ stdin (newline-delimited JSON-RPC 2.0)
magnotia-mcp main.rs loop
→ serde_json::from_str → Value
(or parse_error_response on failure)
→ magnotia_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 magnotia-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. Magnotia'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_threadTokio 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 notracingsetup gating the writers. - Migrations are skipped. The binary's comment at
crates/mcp/src/main.rs:18makes this explicit. The main app is the single migration writer. If the main app has not been run yet (no DB exists),init_readonlywill fail and the binary exits. - Notifications are dropped silently. The MCP spec sends
notifications/initializedafter 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
- MCP tools
- Slice README — MCP auth gap
- Slice 5 (forthcoming) —
magnotia-storage::init_readonly,database_path,list_transcripts,get_transcript,search_transcripts,list_tasks