MCP System
Depends On: Page System, Embedding System, Search System
Overview
Section titled “Overview”The MCP (Model Context Protocol) System provides structured access to workspace data for AI writing assistants.
Request Flow
Section titled “Request Flow”It runs an in-process axum HTTP server inside the Tauri desktop application, exposing workspace operations via the MCP
protocol using Streamable HTTP transport. Agent clients (Claude Code, Cursor, Claude Desktop) connect over
http://127.0.0.1:<port>/mcp.
Key design constraints:
- In-process, not a sidecar — shares AppState with the Tauri app, eliminating IPC overhead.
- Localhost only — binds to
127.0.0.1, never exposed to the network. - Bearer token authentication on every request, with constant-time comparison to prevent timing attacks.
- Premium feature — requires active subscription.
Architecture
Section titled “Architecture”Framework (Tauri)├── MCP Commands apps/desktop/src-tauri/src/commands/mcp.rs│ get_mcp_status, get_mcp_token, set_mcp_enabled, regenerate_mcp_token│└── MCP Server Module apps/desktop/src-tauri/src/mcp/ ├── mod.rs McpError enum, error conversions ├── server.rs InklingsService (rmcp handler), McpServer lifecycle ├── state.rs McpState (extracted from AppState) ├── auth.rs Bearer token middleware (constant-time via subtle) ├── resources.rs MCP Resource handlers (tree, page, search) └── tools/ ├── discovery.rs search, get_page_tree, get_backlinks, get_outgoing_links ├── read.rs read_page └── write.rs create_page, update_page_content, update_page_metadata, move_page, rename_page, delete_pageDependencies flow inward: Framework (Tauri commands + MCP server) -> Application (use cases) -> Domain.
Server Lifecycle
Section titled “Server Lifecycle”Startup
Section titled “Startup”- User opens a workspace (
open_workspaceTauri command). start_mcp_server_if_enabled()checksSettings.mcp_enabled.- If enabled,
McpServer::start()is called with the configured port and persisted token. - If no token exists (first run) or
force_new_tokenis true, a 256-bit CSPRNG hex token is generated. - The token is persisted back to
Settings.mcp_tokenso it survives app restarts. - The axum server binds to
127.0.0.1:{port}and spawns a long-lived tokio task.
Shutdown
Section titled “Shutdown”The server stops in any of these scenarios:
- Workspace is closed (server handle is dropped).
- MCP is disabled via
set_mcp_enabled(false). - Token is regenerated via
regenerate_mcp_token(stop + restart with new token). - App exits (Drop impl sends shutdown signal via oneshot channel).
Graceful shutdown is implemented via axum::serve(...).with_graceful_shutdown().
Token Regeneration
Section titled “Token Regeneration”regenerate_mcp_token stops the current server, then restarts with force_new_token = true. The old token is
immediately invalidated. The new token is persisted to settings.
McpState
Section titled “McpState”McpState (state.rs) extracts the subset of AppState needed by MCP tool handlers:
| Field | Type | Purpose |
|---|---|---|
page_repository | Arc<SqlitePageRepository> | Page CRUD |
reference_repository | SqliteReferenceRepository | Wiki-link index |
embedding_repository | Arc<SqliteEmbeddingRepository> | Semantic search |
search_router | Arc<Mutex<Option<SearchRouter<...>>>> | Unified search |
current_workspace | Arc<Mutex<Option<Workspace>>> | Active workspace |
embedding_provider | Arc<Mutex<Option<Arc<EmbedProvider>>>> | One-shot search router construction |
capability_resolver | Arc<CapabilityResolver<...>> | Permission guards |
The MCP server always runs locally as the workspace owner, so resolve_owner_guard() takes the owner path in
CapabilityResolver — full capabilities, no cloud round-trip. A LocalOwnerCapabilityRepo stub is never actually
invoked.
11 tools organized by category, all dispatched to tokio::task::spawn_blocking since the underlying use cases perform
synchronous SQLite I/O.
Discovery (read-only)
Section titled “Discovery (read-only)”| Tool | Description |
|---|---|
search | Full-text + semantic search via SearchRouter (limit 20) |
get_page_tree | Full hierarchical page tree as JSON |
get_backlinks | Pages linking to a given page via wiki-links |
get_outgoing_links | Outgoing wiki-links from a page |
| Tool | Description |
|---|---|
read_page | Full page detail: title, content, metadata, word count, link counts, timestamps |
| Tool | Description |
|---|---|
create_page | Create page with optional content and parent |
update_page_content | Replace page body (creates fresh LoroDoc, intentionally discards CRDT history) |
update_page_metadata | Update title and/or icon without changing content |
move_page | Move page to new parent or root |
rename_page | Rename page and update all wiki-links workspace-wide |
delete_page | Soft-delete (move to trash) with cascade to children |
Resources
Section titled “Resources”MCP Resources provide read-only context injection via URI-based access:
| URI | Format | Description |
|---|---|---|
inklings://workspace/tree | JSON | Full page hierarchy (always available) |
inklings://workspace/page/{slug} | Markdown | Individual page content |
inklings://workspace/search?q={query} | JSON | Search results with slugs, titles, snippets, scores |
Resource templates allow MCP clients to construct URIs for parameterized access. The read_resource handler dispatches
by URI path prefix.
Authentication
Section titled “Authentication”Bearer Token Middleware
Section titled “Bearer Token Middleware”Every request must carry an Authorization: Bearer <token> header. The middleware (auth.rs) uses
subtle::ConstantTimeEq for comparison to prevent timing side-channel attacks. Missing or invalid tokens return
401 Unauthorized.
// Constant-time comparisonlet token_bytes = token.as_bytes();let expected_bytes = expected.0.as_bytes();if token_bytes.len() != expected_bytes.len() || token_bytes.ct_eq(expected_bytes).unwrap_u8() != 1{ return Err(StatusCode::UNAUTHORIZED);}Token Lifecycle
Section titled “Token Lifecycle”- Generated once on first MCP enable, persisted to
Settings.mcp_token. - Reused across app restarts (loaded from settings on server start).
- Explicitly regenerated via
regenerate_mcp_tokencommand. - Shown in app settings for the user to copy into their MCP client configuration.
- The
get_mcp_statuscommand intentionally excludes the token to avoid leaking it on every poll. The separateget_mcp_tokencommand returns it only on explicit user request.
Settings
Section titled “Settings”MCP settings are part of the domain Settings struct (schema v5):
| Field | Type | Default | Description |
|---|---|---|---|
mcp_enabled | bool | false | Whether the MCP server starts on workspace open |
mcp_port | u16 | 7862 | Localhost port (clamped to 1024-65535) |
mcp_token | Option<String> | None | Persisted bearer token |
Port validation clamps to [MIN_MCP_PORT, MAX_MCP_PORT] to prevent binding to privileged ports.
Tauri Commands
Section titled “Tauri Commands”| Command | Description |
|---|---|
get_mcp_status | Returns running state, safe config (no token), enabled flag, port |
get_mcp_token | Returns bearer token only on explicit request |
set_mcp_enabled | Toggle MCP on/off; starts/stops server immediately |
regenerate_mcp_token | Invalidates old token, restarts server with new one |
Key Design Decisions
Section titled “Key Design Decisions”1. In-Process, Not a Sidecar
Section titled “1. In-Process, Not a Sidecar”The MCP server runs inside the Tauri process, sharing AppState directly. This eliminates IPC complexity and latency.
The trade-off is that the app must be running for MCP access — this is accepted because MCP is for local agent
integration where the user is actively working.
2. Fresh LoroDoc on Agent Writes
Section titled “2. Fresh LoroDoc on Agent Writes”When an agent updates page content via update_page_content, the tool creates a brand-new LoroDoc from the provided
text rather than merging into the existing CRDT history. An agent write is treated as a new edit session. This avoids
the complexity of MCP clients needing to understand CRDT operations.
3. Owner-Only Permission Model
Section titled “3. Owner-Only Permission Model”The MCP server resolves permissions via CapabilityResolver::resolve_owner(), granting all capabilities without a cloud
round-trip. This is correct because the MCP server only runs locally on the workspace owner’s machine. Non-owner access
would come through the sync protocol (remote agents), not MCP.
4. State Snapshot (Not Shared Reference)
Section titled “4. State Snapshot (Not Shared Reference)”McpState snapshots search_router, current_workspace, and embedding_provider from AppState at construction
time. This means the MCP server may miss updates loaded after server start. This is acceptable because the MCP server is
restarted on every workspace change.
Error Handling
Section titled “Error Handling”McpError variants map to semantic JSON-RPC error codes:
| McpError | JSON-RPC Code |
|---|---|
NotFound | RESOURCE_NOT_FOUND |
Validation | INVALID_PARAMS |
Permission | INVALID_REQUEST |
NoWorkspace | Internal error |
Internal / Server | Internal error |
Conversions from PageRepositoryError use UserFacingError::user_message() for human-readable messages.
Tools API Reference
Section titled “Tools API Reference”Connection
Section titled “Connection”| Property | Value |
|---|---|
| Default URL | http://127.0.0.1:7862/mcp |
| Health check | GET http://127.0.0.1:7862/health (unauthenticated) |
| Transport | Streamable HTTP (JSON-RPC) |
| Auth | Authorization: Bearer <token> |
| Request timeout | 60 seconds |
The token is a CSPRNG 256-bit hex string (64 characters) generated on server start, displayed in the Inklings app settings panel. Token comparison uses constant-time equality (subtle::ConstantTimeEq).
MCP client configuration example (Claude Desktop claude_desktop_config.json):
{ "mcpServers": { "inklings": { "url": "http://127.0.0.1:7862/mcp", "headers": { "Authorization": "Bearer <token-from-app-settings>" } } }}Tools Inventory
Section titled “Tools Inventory”| Tool | Category | Description |
|---|---|---|
search | Discovery | Search workspace pages by content and title |
get_page_tree | Discovery | Get the full hierarchical page tree |
get_backlinks | Discovery | Get all pages that link to a specified page |
get_outgoing_links | Discovery | Get all outgoing wiki-links from a page |
read_page | Read | Read a page’s full content and metadata |
create_page | Write | Create a new page in the workspace |
update_page_content | Write | Replace a page’s text content |
update_page_metadata | Write | Update a page’s title or icon without changing content |
move_page | Write | Move a page to a new parent or to root |
rename_page | Write | Rename a page and auto-update all wiki-links |
delete_page | Write | Soft-delete a page with cascade to children |
Resources
Section titled “Resources”| URI / Template | Name | MIME Type | Description |
|---|---|---|---|
inklings://workspace/tree | Page Tree | application/json | Full page hierarchy as JSON |
inklings://workspace/page/{slug} | Page Content | text/markdown | Read a page’s content by slug |
inklings://workspace/search?q={query} | Search Results | application/json | Search workspace pages by query |
Capability Matrix
Section titled “Capability Matrix”All 11 tools require owner-level permissions. The MCP server runs on the local machine as the workspace owner, so permission resolution is local-only with no cloud round-trip.
Related
Section titled “Related”- Embedding System — Semantic search used by the
searchtool and search resource. - Event Log System — Page history and timeline available for agent tooling.
- Page System — All page operations are delegated to page use cases.
Was this page helpful?
Thanks for your feedback!