# MCP Integration Guide **New in v1.0.0-rc.2.** ClioDeck speaks the [Model Context Protocol](https://modelcontextprotocol.io) in both directions: - **Client side** — Brainstorm can call tools exposed by external MCP servers you configure. - **Server side** — ClioDeck exposes your indexed corpus to Claude Desktop, Claude Code, or any other MCP client. Both surfaces are opt-in. The MCP server is **inactive by default**. --- ## Part A — MCP clients consumed by ClioDeck ClioDeck's Brainstorm mode can drive any MCP server (stdio or SSE transport) the user configures. The tools that server exposes become callable by the LLM during a Brainstorm turn (provider must advertise `capabilities.tools`). ### Configuration **Settings → MCP Clients** (`MCPClientsSection`): 1. Click **Add client**. 2. Fill in: - **Name** — a short identifier (no spaces; this becomes part of the namespaced tool name `__`). - **Transport** — `stdio` (spawn a subprocess) or `sse` (connect to an HTTP endpoint). - For `stdio`: **Command** and **Args** (space-separated). - For `sse`: **URL**. 3. Save. ClioDeck spawns the subprocess (or opens the SSE stream) and runs the MCP handshake. ### Lifecycle and state Each client exposes a typed state, not a `connected: bool`: | State | Meaning | |---|---| | `unconfigured` | Configured but not yet started. | | `spawning` | Subprocess started; transport handshake in progress. | | `handshaking` | Transport up; MCP `initialize` round-trip in progress. | | `ready` | Tools listed, available to Brainstorm. | | `degraded` | Operational but partially impaired. | | `failed` | Last error captured in `lastError`. | | `stopped` | Explicitly stopped by the user. | The UI reflects the state in real time. A crashed client is auto-retried once, **without any user action required** — but this isn't invisible in the UI: the client transitions through `degraded` (shown with the same red/danger color as `failed`, raw state text) for the ~500ms retry window (`backend/integrations/mcp-clients/manager.ts`), then either recovers to `ready` or, on a second failure, moves to `failed` with a manual retry button. What's automatic is the *retry attempt*, not its visibility. Partial success is first-class: if 3 clients out of 5 are `ready` and 2 are `failed`, Brainstorm uses the 3 and shows a badge for the missing 2. ### How tools reach the LLM In a Brainstorm turn (`fusion-chat-service.ts`), the engine assembles a catalog of tools by iterating ready clients: ```text namespaced name = __ description = [] parameters = the tool's JSON Schema (from its inputSchema) ``` When the LLM emits a tool call, the engine strips the namespace prefix and routes the call to the right client via `mcpClientsService.callTool(clientName, bareTool, args)`. Before the catalog reaches the model, Brainstorm applies a **read/write opt-in filter**: a pure heuristic (`backend/integrations/mcp-clients/tool-classifier.ts`) classifies each bare tool name as `'read'` (auto-enabled — `search_`/`get_`/`list_`/`find_`/`fetch_`/… prefixes, plus `ping`/`health`/`version`) or `'write'` (opt-in — everything else, including any unrecognised verb). All 9 of ClioDeck's own built-in tools classify as `read`. The user's per-tool choices are surfaced in a banner above the Brainstorm chat input and persist across restarts — see [Brainstorm Mode Guide](./1.11-Brainstorm-Mode-Guide.md#mcp-tools-banner-read-auto-enabled-write-opt-in). Passing no `enabledTools` at all (any caller other than Brainstorm) preserves the legacy behaviour of every ready tool being available. --- ## Part B — The MCP server ClioDeck exposes ClioDeck ships an MCP server binary (`bin/cliodeck-mcp`) that exposes the workspace's indexed corpus + a handful of public-archive connectors as MCP tools. It is **off by default** — `loadMCPConfig` refuses to start unless the workspace's `config.json` has the server enabled. ### Activate the server **Settings → MCP Server** (`MCPServerSection`): 1. Tick the **Enable** checkbox. The status badge flips between `on` and `off`. 2. (Optional) Change the server name. Default: the workspace folder name. 3. The section displays the resolved **Binary path** (read-only); the resolution depends on whether you're running a packaged build or `npm start`. Copy it from the UI — you'll paste it into your MCP client config. 4. Pick a client tab (**Claude Desktop**, **Claude Code CLI**, or **Generic MCP (stdio)**) and copy the generated snippet. Every call into the server is appended to `.cliodeck/mcp-access.jsonl` as a typed `MCPAccessEvent` (kind, tool name, timestamp, input, summary of output). Open the file directly with any text editor to audit which client called what, when (a dedicated viewer is on the RC3 roadmap). ### The 9 tools exposed All tools return JSON-friendly content. `topK` defaults to 10 and is bounded between 1 and 200. Content is truncated to 4,000 characters per chunk (2,000 for Gallica/HAL snippets) to keep responses bounded. | Tool | Purpose | Required input | Optional input | |---|---|---|---| | `search_documents` | Lexical search across PDF chunks (full text). | `query` (string) | `year`, `author`, `topK` | | `search_obsidian` | BM25 search across your Obsidian vault (FTS5). | `query` (string) | `topK` | | `search_tropy` | Case-insensitive search over Tropy primary sources. | `query` (string) | `topK` | | `search_zotero` | Lookup over bibliography (title / author / bibtex key). | `query` (string) | `year`, `topK` | | `graph_neighbors` | Citations out, citations in, similarity neighbors for a document. | `documentId` (string) | `topK` | | `entity_context` | NER-extracted mentions of a named entity across the corpus. | `entity` (string) | `topK` | | `search_gallica` | SRU search against the BnF's Gallica catalogue. No key required. | `query` (string) | `dateFrom`, `dateTo`, `docType`, `topK` | | `search_hal` | Solr search against the HAL open-archive (CCSD). No key required. | `query` (string) | `dateFrom`, `dateTo`, `docType`, `topK` | | `search_europeana` | Search Europeana (~50M items). **Requires a free API key.** | `query` (string) | `mediaType` (IMAGE/TEXT/VIDEO/SOUND/3D), `topK` | The Europeana key is read at *call* time from the `EUROPEANA_API_KEY` environment variable. When ClioDeck spawns the MCP server itself, the key is propagated automatically from `secureStorage`. When a third-party MCP client spawns the server, you need to set the env var in your client config — see the snippets below. --- ## Part C — Connect external MCP clients to ClioDeck The Settings → MCP Server section generates ready-to-paste snippets for three common clients. Copy them from the UI for the live binary path; what follows is the shape. ### Claude Desktop Edit `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or the equivalent on your OS, and merge: ```json { "mcpServers": { "cliodeck": { "command": "/absolute/path/to/cliodeck-mcp", "args": ["/absolute/path/to/your/workspace"] } } } ``` The exact `command` path is shown in **Settings → MCP Server → Binary path** — copy it from there. In dev it resolves to `/bin/cliodeck-mcp`; in a packaged build it sits under Electron's `resources/bin/cliodeck-mcp`. To enable Europeana from Claude Desktop, add an `env` block to the snippet: ```json { "mcpServers": { "cliodeck": { "command": "...", "args": ["..."], "env": { "EUROPEANA_API_KEY": "your-wskey-here" } } } } ``` Restart Claude Desktop after editing. ### Claude Code CLI From a terminal: ```bash claude mcp add cliodeck -- /path/to/cliodeck-mcp /absolute/path/to/your/workspace ``` To pass `EUROPEANA_API_KEY`, prefix the command with `env`: ```bash claude mcp add cliodeck -- env EUROPEANA_API_KEY=your-key /path/to/cliodeck-mcp /absolute/path/to/your/workspace ``` ### Generic MCP (stdio) Any MCP client that supports stdio can spawn the binary directly: ```bash /path/to/cliodeck-mcp /absolute/path/to/your/workspace ``` The first positional argument is **always** the workspace root (the folder that contains `.cliodeck/`). --- ## Security model - **Server inactive by default.** No data leaves the workspace until you flip the toggle. - **Per-workspace scope.** The binary is bound to one workspace root passed on the command line; it cannot see other projects. - **Auditable.** Every call is appended to `.cliodeck/mcp-access.jsonl` as a typed event. There is no renderer-side viewer for it yet (see the note above) — open the file directly to audit it. - **Revocable.** Toggling the server off invalidates running connections; restarting the client is required to re-connect. - **No secrets on disk.** API keys (Europeana) live in Electron's `safeStorage`, never in `config.json`. For external MCP clients you wire *into* ClioDeck, the same source-inspection pipeline that runs on PDFs and Tropy chunks (`backend/security/source-inspector.ts`) is applied to tool results before they enter the LLM prompt. > **⚠️ Real bug, verified against code:** an in-flight tool call itself is > safe across a project switch — `MCPClientsService.callTool()` > (`mcp-clients-service.ts`) never re-reads `this.workspaceRoot` mid-call, > it just delegates to whichever manager instance was already in scope when > the call started. But the **audit log** it's recorded to can still be > misattributed: every manager's `onEvent` closure calls > `this.writeAuditLog(e)`, which reads `this.auditLogStream` — the > **current** field value — not a reference captured when that manager was > created. `loadProject()`'s `unload()` step calls `manager.stopAll()` > before opening the new project's audit stream, but `stop()` > (`manager.ts`) only awaited `handle.close()`, so an in-flight tool call's > audit event could land in the **new** project's `mcp-access.jsonl`. > **Fixed in RC4** — the audit stream is captured when the manager is > created, so it can never write to another project's trail. --- ## See also - [Brainstorm Mode Guide](./1.11-Brainstorm-Mode-Guide.md) — how MCP tools are dispatched inside the agent loop. - [Archive Connectors Guide](./1.13-Archive-Connectors-Guide.md) — the three archive tools in detail. - [Obsidian Vault Guide](./1.14-Obsidian-Vault-Guide.md) — the source behind `search_obsidian`.