Skip to content
Documentation GitHub
Agent

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_workspace before 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, and get_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 raw fetch() calls to http://127.0.0.1:{port}/mcp once 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:

  1. Initialize a workspace.
  2. Call get_mcp_status via the bridge.
  3. Observe the returned McpStatus object.

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:

  1. Initialize a workspace.
  2. Call set_mcp_enabled with enabled: true.
  3. 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:

  1. Initialize a workspace.
  2. Enable the server via set_mcp_enabled(true).
  3. Confirm it is running via get_mcp_status.
  4. Call set_mcp_enabled(false).
  5. 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:

  1. Initialize a workspace.
  2. Enable the server via set_mcp_enabled(true).
  3. Call get_mcp_status.
  4. 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:

  1. Initialize a workspace.
  2. Enable the server via set_mcp_enabled(true).
  3. Call get_mcp_token and record the original token as token_v1.
  4. Call regenerate_mcp_token.
  5. Call get_mcp_token and record the new token as token_v2.
  6. 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:

  1. Initialize a workspace and enable the MCP server.
  2. Call get_mcp_token to retrieve the token.
  3. Send a POST request to http://127.0.0.1:7862/mcp with Authorization: Bearer <token> and a valid MCP initialize message 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:

  1. Initialize a workspace and enable the MCP server.
  2. Send a POST request to http://127.0.0.1:7862/mcp with Authorization: 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:

  1. Initialize a workspace and enable the MCP server.
  2. Send a POST request to http://127.0.0.1:7862/mcp with no Authorization header.

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:

  1. Initialize a workspace and enable the MCP server.
  2. Send a GET request to http://127.0.0.1:7862/health with no Authorization header.

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:

  1. Initialize a workspace and enable the MCP server.
  2. Call the MCP create_page tool with title: "MCP Test Page" and content: "Created by MCP".
  3. After a successful tool response, call get_page via 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:

  1. Initialize a workspace.
  2. Create a page titled “Agent Reference” via the bridge.
  3. Enable the MCP server.
  4. Call the MCP read_page tool with slug: "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:

  1. Initialize a workspace.
  2. Create two pages via the bridge: “Dragon Lore” with content “Dragons breathe fire” and “Dungeon Map” with content “The dungeon has many corridors”.
  3. Enable the MCP server.
  4. Call the MCP search tool with query: "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:

  1. Initialize a workspace.
  2. Create a page titled “Deletable” via the bridge.
  3. Enable the MCP server.
  4. Call the MCP delete_page tool with slug: "deletable".
  5. Call get_page via 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:

  1. Initialize a workspace.
  2. Create pages “Parent Page” and “Child Page” as a child of “Parent Page” via the bridge.
  3. Enable the MCP server.
  4. Call read_resource with 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:

  1. 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).
  2. Open (initialize) a workspace.
  3. 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:

  1. Initialize a workspace and enable the MCP server.
  2. Confirm the server is running via get_mcp_status.
  3. Close (or switch away from) the workspace via the bridge.
  4. 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:

  1. Initialize a workspace.
  2. Call update_settings (or equivalent) to set mcp_port: 7900.
  3. Enable the server via set_mcp_enabled(true).
  4. Call get_mcp_status.
  5. 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:

  1. Initialize a workspace.
  2. Attempt to call update_settings (or equivalent) with mcp_port: 80.
  3. 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

KeyValueNotes
default_port7862INKL on phone keypad — hardcoded default in domain settings
custom_port7900Alternative port for port-configuration scenarios
min_mcp_port1024Lowest non-privileged port (clamped by set_mcp_port)
token_pattern[0-9a-f]{64}256-bit CSPRNG hex token format
health_endpointhttp://127.0.0.1:7862/healthUnauthenticated readiness probe
mcp_endpointhttp://127.0.0.1:7862/mcpProtected Streamable HTTP MCP endpoint
tree_resource_uriinklings://workspace/treeFixed resource URI for full workspace hierarchy
page_resource_prefixinklings://workspace/page/Resource template prefix for per-page reads
search_resource_uriinklings://workspace/search?q=Resource template for search queries
test_page_titleMCP Test PagePage created through MCP tool in scenario 10
test_page_slugmcp-test-pageExpected 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 raw fetch() calls to localhost:{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_status intentionally omits the token from its response. Callers must invoke get_mcp_token separately. 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_page via MCP creates a real page in SQLite — the bridge can confirm it via get_page after the tool call.
  • delete_page performs 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.1 origin with GET and POST methods and Authorization/Content-Type headers. 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 see McpError::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?