Skip to content
Documentation GitHub
Agent

MCP System

Depends On: Page System, Embedding System, Search System


The MCP (Model Context Protocol) System provides structured access to workspace data for AI writing assistants.

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.

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_page

Dependencies flow inward: Framework (Tauri commands + MCP server) -> Application (use cases) -> Domain.


  1. User opens a workspace (open_workspace Tauri command).
  2. start_mcp_server_if_enabled() checks Settings.mcp_enabled.
  3. If enabled, McpServer::start() is called with the configured port and persisted token.
  4. If no token exists (first run) or force_new_token is true, a 256-bit CSPRNG hex token is generated.
  5. The token is persisted back to Settings.mcp_token so it survives app restarts.
  6. The axum server binds to 127.0.0.1:{port} and spawns a long-lived tokio task.

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().

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 (state.rs) extracts the subset of AppState needed by MCP tool handlers:

FieldTypePurpose
page_repositoryArc<SqlitePageRepository>Page CRUD
reference_repositorySqliteReferenceRepositoryWiki-link index
embedding_repositoryArc<SqliteEmbeddingRepository>Semantic search
search_routerArc<Mutex<Option<SearchRouter<...>>>>Unified search
current_workspaceArc<Mutex<Option<Workspace>>>Active workspace
embedding_providerArc<Mutex<Option<Arc<EmbedProvider>>>>One-shot search router construction
capability_resolverArc<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.

ToolDescription
searchFull-text + semantic search via SearchRouter (limit 20)
get_page_treeFull hierarchical page tree as JSON
get_backlinksPages linking to a given page via wiki-links
get_outgoing_linksOutgoing wiki-links from a page
ToolDescription
read_pageFull page detail: title, content, metadata, word count, link counts, timestamps
ToolDescription
create_pageCreate page with optional content and parent
update_page_contentReplace page body (creates fresh LoroDoc, intentionally discards CRDT history)
update_page_metadataUpdate title and/or icon without changing content
move_pageMove page to new parent or root
rename_pageRename page and update all wiki-links workspace-wide
delete_pageSoft-delete (move to trash) with cascade to children

MCP Resources provide read-only context injection via URI-based access:

URIFormatDescription
inklings://workspace/treeJSONFull page hierarchy (always available)
inklings://workspace/page/{slug}MarkdownIndividual page content
inklings://workspace/search?q={query}JSONSearch 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.


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 comparison
let 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);
}
  • 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_token command.
  • Shown in app settings for the user to copy into their MCP client configuration.
  • The get_mcp_status command intentionally excludes the token to avoid leaking it on every poll. The separate get_mcp_token command returns it only on explicit user request.

MCP settings are part of the domain Settings struct (schema v5):

FieldTypeDefaultDescription
mcp_enabledboolfalseWhether the MCP server starts on workspace open
mcp_portu167862Localhost port (clamped to 1024-65535)
mcp_tokenOption<String>NonePersisted bearer token

Port validation clamps to [MIN_MCP_PORT, MAX_MCP_PORT] to prevent binding to privileged ports.


CommandDescription
get_mcp_statusReturns running state, safe config (no token), enabled flag, port
get_mcp_tokenReturns bearer token only on explicit request
set_mcp_enabledToggle MCP on/off; starts/stops server immediately
regenerate_mcp_tokenInvalidates old token, restarts server with new one

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.

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.

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.

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.


McpError variants map to semantic JSON-RPC error codes:

McpErrorJSON-RPC Code
NotFoundRESOURCE_NOT_FOUND
ValidationINVALID_PARAMS
PermissionINVALID_REQUEST
NoWorkspaceInternal error
Internal / ServerInternal error

Conversions from PageRepositoryError use UserFacingError::user_message() for human-readable messages.


PropertyValue
Default URLhttp://127.0.0.1:7862/mcp
Health checkGET http://127.0.0.1:7862/health (unauthenticated)
TransportStreamable HTTP (JSON-RPC)
AuthAuthorization: Bearer <token>
Request timeout60 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>"
}
}
}
}
ToolCategoryDescription
searchDiscoverySearch workspace pages by content and title
get_page_treeDiscoveryGet the full hierarchical page tree
get_backlinksDiscoveryGet all pages that link to a specified page
get_outgoing_linksDiscoveryGet all outgoing wiki-links from a page
read_pageReadRead a page’s full content and metadata
create_pageWriteCreate a new page in the workspace
update_page_contentWriteReplace a page’s text content
update_page_metadataWriteUpdate a page’s title or icon without changing content
move_pageWriteMove a page to a new parent or to root
rename_pageWriteRename a page and auto-update all wiki-links
delete_pageWriteSoft-delete a page with cascade to children
URI / TemplateNameMIME TypeDescription
inklings://workspace/treePage Treeapplication/jsonFull page hierarchy as JSON
inklings://workspace/page/{slug}Page Contenttext/markdownRead a page’s content by slug
inklings://workspace/search?q={query}Search Resultsapplication/jsonSearch workspace pages by query

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.


  • Embedding System — Semantic search used by the search tool 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?