MCP Server Management
MCP Server Management
Covers the full lifecycle of the in-process MCP (Model Context Protocol) server: enabling and disabling the server via settings, status display, bearer token management, authentication enforcement, the complete set of page tools and resources exposed to MCP clients, workspace-lifecycle coupling (start on open, stop on close), port configuration, and concurrent request handling. This spec is P1 because MCP is the primary integration surface for external AI writing tools — a broken token, a server that fails to start, or a tool that returns malformed data renders the entire integration useless for power users who rely on it.
The MCP server runs in-process inside the Tauri app, binding to 127.0.0.1:{port}/mcp over Streamable HTTP. Bearer
token authentication protects all /mcp endpoints; a separate /health endpoint is unauthenticated. MCP is an opt-in
premium feature — mcp_enabled defaults to false.
Preconditions
- HTTP bridge running on port 9990
- A workspace initialized via
initialize_workspacebefore each scenario - Bridge shim injected via
playwright.config.ts - The HTTP bridge exposes MCP management commands:
get_mcp_status,set_mcp_enabled,regenerate_mcp_token, andget_mcp_token. Status, configuration, enable/disable, and token management scenarios (1–5, 15–18) are fully exercisable via the bridge. Direct MCP protocol scenarios (bearer auth, tool calls) must be executed by the test agent making rawfetch()calls tohttp://127.0.0.1:{port}/mcponce the server is running — these are marked[direct-http].
Scenarios
Seed: seed.spec.ts
1. Default MCP status — disabled with no token
On a fresh workspace, MCP is disabled and no token has been generated.
Steps:
- Initialize a workspace.
- Call
get_mcp_statusvia the bridge. - Observe the returned
McpStatusobject.
Expected: running is false. enabled is false. config is null. port is 7862 (the default). No bearer
token exists yet.
2. Enable the MCP server from settings
Calling set_mcp_enabled(true) with an open workspace starts the server immediately.
Steps:
- Initialize a workspace.
- Call
set_mcp_enabledwithenabled: true. - Call
get_mcp_status.
Expected: set_mcp_enabled returns an updated McpStatus with running: true, enabled: true, and a non-null
config object. config.url is "http://127.0.0.1:7862/mcp". config.has_token is true. config.port is 7862.
The subsequent get_mcp_status call confirms the same state.
3. Disable the MCP server from settings
Calling set_mcp_enabled(false) stops a running server.
Steps:
- Initialize a workspace.
- Enable the server via
set_mcp_enabled(true). - Confirm it is running via
get_mcp_status. - Call
set_mcp_enabled(false). - Call
get_mcp_status.
Expected: After step 4, the returned status has running: false, enabled: false, config: null. The subsequent
get_mcp_status call confirms running: false. The server is no longer accessible at the MCP endpoint.
4. Token is withheld from status responses — requires explicit get_mcp_token call
get_mcp_status returns has_token: true but never the token value, to avoid leaking the secret on every poll.
Steps:
- Initialize a workspace.
- Enable the server via
set_mcp_enabled(true). - Call
get_mcp_status. - Call
get_mcp_token.
Expected: get_mcp_status returns config.has_token: true but no token field in config. get_mcp_token
returns the actual token string (a 64-character lowercase hex string — 256 bits from CSPRNG). The token is non-empty and
matches the pattern [0-9a-f]{64}.
5. Token regeneration invalidates the old token
regenerate_mcp_token stops and restarts the server with a freshly generated token.
Steps:
- Initialize a workspace.
- Enable the server via
set_mcp_enabled(true). - Call
get_mcp_tokenand record the original token astoken_v1. - Call
regenerate_mcp_token. - Call
get_mcp_tokenand record the new token astoken_v2. - Confirm the server is still running via
get_mcp_status.
Expected: token_v2 is different from token_v1 — they are not equal. get_mcp_status after step 4 shows
running: true. regenerate_mcp_token returns the same McpStatus shape as get_mcp_status. The old token token_v1
is no longer valid for requests (see scenario 8).
6. Bearer token authentication — valid token accepted [direct-http]
A request to /mcp with the correct Authorization: Bearer <token> header succeeds.
Steps:
- Initialize a workspace and enable the MCP server.
- Call
get_mcp_tokento retrieve the token. - Send a POST request to
http://127.0.0.1:7862/mcpwithAuthorization: Bearer <token>and a valid MCPinitializemessage body.
Expected: The server responds with HTTP 200. The response body is a valid MCP JSON-RPC response (contains result
or follows the MCP initialization handshake format). No 401 or 403 status is returned.
7. Bearer token authentication — invalid token rejected [direct-http]
A request with an incorrect token is rejected with HTTP 401.
Steps:
- Initialize a workspace and enable the MCP server.
- Send a POST request to
http://127.0.0.1:7862/mcpwithAuthorization: Bearer wrong-token-here.
Expected: The server responds with HTTP 401 Unauthorized. The response body (if any) does not contain workspace data. The server does not crash or return 500.
8. Bearer token authentication — missing Authorization header rejected [direct-http]
A request with no Authorization header is rejected with HTTP 401.
Steps:
- Initialize a workspace and enable the MCP server.
- Send a POST request to
http://127.0.0.1:7862/mcpwith noAuthorizationheader.
Expected: The server responds with HTTP 401 Unauthorized. The request is rejected before any MCP processing occurs.
9. Health endpoint is unauthenticated [direct-http]
The /health endpoint responds without a bearer token.
Steps:
- Initialize a workspace and enable the MCP server.
- Send a GET request to
http://127.0.0.1:7862/healthwith noAuthorizationheader.
Expected: The server responds with HTTP 200. The response body is JSON containing at least {"status": "ok"}. The
version field matches the app version string.
10. MCP tools — create_page creates a page visible in the workspace [direct-http]
The create_page MCP tool creates a real page in the open workspace.
Steps:
- Initialize a workspace and enable the MCP server.
- Call the MCP
create_pagetool withtitle: "MCP Test Page"andcontent: "Created by MCP". - After a successful tool response, call
get_pagevia the bridge with the returned slug.
Expected: The create_page tool returns a JSON CallToolResult containing the new page’s slug, title, and
page_id. The get_page bridge call confirms the page exists with the correct title and content. The result follows
the MCP CallToolResult format (array of content items).
11. MCP tools — read_page returns page content and metadata [direct-http]
The read_page MCP tool returns a page’s full content and metadata.
Steps:
- Initialize a workspace.
- Create a page titled “Agent Reference” via the bridge.
- Enable the MCP server.
- Call the MCP
read_pagetool withslug: "agent-reference".
Expected: The tool returns a CallToolResult containing the page’s title, content, word count, and timestamps. The
content matches what was entered via the bridge. No authentication error occurs.
12. MCP tools — search finds pages by content [direct-http]
The search MCP tool returns relevant pages for a query.
Steps:
- Initialize a workspace.
- Create two pages via the bridge: “Dragon Lore” with content “Dragons breathe fire” and “Dungeon Map” with content “The dungeon has many corridors”.
- Enable the MCP server.
- Call the MCP
searchtool withquery: "dragon fire".
Expected: The search tool returns a CallToolResult with a JSON array of search hits. At least one hit has
slug: "dragon-lore" and a non-empty snippet. Each hit contains slug, title, snippet, and score fields.
“Dungeon Map” does not appear as the top result.
13. MCP tools — delete_page performs soft delete [direct-http]
The delete_page MCP tool moves a page to the trash, not permanent deletion.
Steps:
- Initialize a workspace.
- Create a page titled “Deletable” via the bridge.
- Enable the MCP server.
- Call the MCP
delete_pagetool withslug: "deletable". - Call
get_pagevia the bridge with slug"deletable".
Expected: The delete_page tool returns a success CallToolResult. The get_page bridge call returns a “not
found” error (the page is no longer in the active workspace). The page was moved to the trash — it was not permanently
destroyed. It is recoverable via restore_page.
14. MCP resources — get_page_tree resource returns workspace hierarchy [direct-http]
The inklings://workspace/tree resource returns the full workspace hierarchy as JSON.
Steps:
- Initialize a workspace.
- Create pages “Parent Page” and “Child Page” as a child of “Parent Page” via the bridge.
- Enable the MCP server.
- Call
read_resourcewith URI"inklings://workspace/tree".
Expected: The resource response contains JSON with mime_type: "application/json". The JSON array includes a node
for “Parent Page” that has has_children: true and a nested children array containing a node for “Child Page”.
15. MCP server starts automatically when workspace is opened with MCP enabled
If mcp_enabled is true in settings, opening a workspace starts the server without any additional command.
Steps:
- Enable MCP in settings before opening a workspace (
set_mcp_enabled(true)while no workspace is open — verify the server is not yet running since no workspace is open). - Open (initialize) a workspace.
- Call
get_mcp_status.
Expected: After the workspace opens, get_mcp_status returns running: true. The server started automatically
without a separate set_mcp_enabled call after workspace open. This confirms the workspace open lifecycle hook starts
the server when mcp_enabled is persisted in settings.
16. MCP server stops when workspace is closed
Closing the workspace stops the running MCP server.
Steps:
- Initialize a workspace and enable the MCP server.
- Confirm the server is running via
get_mcp_status. - Close (or switch away from) the workspace via the bridge.
- Call
get_mcp_status.
Expected: After the workspace is closed, get_mcp_status returns running: false. The /health endpoint at the
previously configured port no longer responds (connection refused or 503, depending on OS). The server shut down
gracefully without error.
17. Port configuration — custom port is respected
The MCP server binds to a user-configured port, not always the default 7862.
Steps:
- Initialize a workspace.
- Call
update_settings(or equivalent) to setmcp_port: 7900. - Enable the server via
set_mcp_enabled(true). - Call
get_mcp_status. - Send a GET request to
http://127.0.0.1:7900/health.
Expected: get_mcp_status returns config.port: 7900 and config.url: "http://127.0.0.1:7900/mcp". The health
check at port 7900 returns {"status": "ok"}. Port 7862 is not in use by this server instance.
18. Privileged port is rejected during settings update
Setting the MCP port below 1024 must be rejected by the settings validation layer.
Steps:
- Initialize a workspace.
- Attempt to call
update_settings(or equivalent) withmcp_port: 80. - Call
get_mcp_status.
Expected: The settings update returns a validation error (port below MIN_MCP_PORT of 1024). The MCP port remains
at its prior valid value (e.g., 7862). No server binds to port 80.
Test Data
| Key | Value | Notes |
|---|---|---|
| default_port | 7862 | INKL on phone keypad — hardcoded default in domain settings |
| custom_port | 7900 | Alternative port for port-configuration scenarios |
| min_mcp_port | 1024 | Lowest non-privileged port (clamped by set_mcp_port) |
| token_pattern | [0-9a-f]{64} | 256-bit CSPRNG hex token format |
| health_endpoint | http://127.0.0.1:7862/health | Unauthenticated readiness probe |
| mcp_endpoint | http://127.0.0.1:7862/mcp | Protected Streamable HTTP MCP endpoint |
| tree_resource_uri | inklings://workspace/tree | Fixed resource URI for full workspace hierarchy |
| page_resource_prefix | inklings://workspace/page/ | Resource template prefix for per-page reads |
| search_resource_uri | inklings://workspace/search?q= | Resource template for search queries |
| test_page_title | MCP Test Page | Page created through MCP tool in scenario 10 |
| test_page_slug | mcp-test-page | Expected slug derived from title |
Notes
- The MCP server is an in-process Axum server, not a sidecar. Scenarios marked
[direct-http]require the Playwright test agent to issue rawfetch()calls tolocalhost:{port}directly, as those scenarios exercise the MCP protocol endpoint itself rather than the management commands. The management commands (get_mcp_status,set_mcp_enabled,regenerate_mcp_token,get_mcp_token) are wired through the HTTP bridge. - The bearer token uses constant-time comparison (
subtle::ConstantTimeEq) to prevent timing side-channel attacks. Tests should not rely on timing differences to detect valid vs invalid tokens. - Token generation uses
rand::rng().fill()(CSPRNG), so two tokens are astronomically unlikely to collide. Scenario 5 verifies they differ via string equality, not entropy analysis. get_mcp_statusintentionally omits the token from its response. Callers must invokeget_mcp_tokenseparately. This two-command design prevents token leakage on every status poll.- MCP tools call the same application use cases as the Tauri commands. A
create_pagevia MCP creates a real page in SQLite — the bridge can confirm it viaget_pageafter the tool call. delete_pageperforms soft delete (moves to trash) — it is not a permanent deletion. This matches the workspace domain rule for cascade deletion.- The CORS layer allows only
http://127.0.0.1origin with GET and POST methods andAuthorization/Content-Typeheaders. Cross-origin requests from non-localhost origins are blocked. - The server has a 60-second request timeout (
TimeoutLayer). Long-running tool calls return HTTP 408 if they exceed this limit. - MCP is described as a premium feature (Pro tier). The subscription capability check is enforced at the permission
guard layer (
resolve_owner_guard) — tests running with the local owner permission will pass; tests simulating a non-owner without capabilities should seeMcpError::Permission. - When
set_mcp_enabled(true)is called while no workspace is open, settings are saved but the server does NOT start (requires an open workspace). The server starts when the workspace subsequently opens.
Was this page helpful?
Thanks for your feedback!