diff --git a/docs/README.md b/docs/README.md index d6a5a9edb0..b6bb837b72 100644 --- a/docs/README.md +++ b/docs/README.md @@ -46,6 +46,7 @@ For a full walkthrough (installation, onboarding, first task, and daily workflow | Guide | Description | |---|---| | [Settings Reference](./settings-reference.md) | Global/project settings, workflow setting values, model/fallback lane hierarchy, defaults, and API endpoints | +| [MCP](./mcp.md) | Model Context Protocol server configuration, secret references, validation, CLI, dashboard, and import/export workflows | | [Agents](./agents.md) | Agent management, presets, prompts, heartbeat behavior, spawning, and mailbox workflows | ### Architecture & Development diff --git a/docs/cli-reference.md b/docs/cli-reference.md index 2ff7d5d1ae..4023e81709 100644 --- a/docs/cli-reference.md +++ b/docs/cli-reference.md @@ -988,7 +988,7 @@ fn settings import [--scope global|project|both] [--merge] [--yes] ## `fn mcp` -Manage Fusion MCP server definitions for stdio, SSE, and streamable HTTP transports. +Manage Fusion MCP server definitions for stdio, SSE, and streamable HTTP transports. See [MCP](./mcp.md) for the full configuration and usage guide, including dashboard flows and secret-reference behavior. ```bash fn mcp list [--project ] [--json] diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md index 9843a89de3..4ba773017a 100644 --- a/docs/dashboard-guide.md +++ b/docs/dashboard-guide.md @@ -1512,7 +1512,7 @@ Manage project and global secrets directly inside **Settings → Project → Sec ### MCP server management in Settings -Manage Model Context Protocol servers from the existing Settings modal; no new top-level dashboard view is introduced. +Manage Model Context Protocol servers from the existing Settings modal; no new top-level dashboard view is introduced. See [MCP](./mcp.md) for the full setup, validation, CLI, import, and export guide. - **Settings → Global → MCP Servers** stores global MCP defaults shared by projects. - **Settings → Project → MCP Servers** stores project-level MCP settings. Project entries override global servers by matching `name`, and a same-named disabled project entry suppresses the inherited global server. The project list marks inherited, overridden, project-local, and disabled-global states so operators can see which scope owns the effective entry. diff --git a/docs/mcp.md b/docs/mcp.md new file mode 100644 index 0000000000..ba41f23108 --- /dev/null +++ b/docs/mcp.md @@ -0,0 +1,221 @@ +# MCP (Model Context Protocol) + +[← Back to docs index](./README.md) + +MCP (Model Context Protocol) is a standard way to attach external tool servers to AI runtimes. Fusion stores trusted MCP server definitions in settings, resolves the effective global/project configuration, materializes any referenced secrets only at use time, and forwards enabled servers to MCP-capable AI lanes so those lanes can use the same operator-approved tools. + + + +## Overview + +MCP support lets Fusion operators configure external stdio, SSE, or streamable HTTP MCP servers once and make them available to AI sessions that support MCP. Enabled servers are treated as **trusted once configured**: after an operator saves a server definition, Fusion may forward it to supported AI lanes without asking again for each session. + +MCP configuration lives in the `mcpServers` settings key at both scopes: + +- **Global settings** hold shared MCP declarations. +- **Project settings** hold project-specific declarations. +- Effective resolution is project-over-global by server `name`: global servers load first, same-named project servers replace them, and same-named project servers with `enabled:false` disable the inherited global server. +- The project-level `mcpServers.enabled` flag overrides the global flag when it is set. If the effective flag is false, no MCP servers are active. + +Expected outcome: when `mcpServers.enabled` resolves to true and at least one enabled server definition is valid, supported AI runtimes receive the effective server set for new sessions. + +## Server definitions and transports + +Every server has a unique `name`, an optional per-server `enabled` flag, and exactly one transport: + +| Transport | Required fields | Optional sensitive fields | Notes | +|---|---|---|---| +| `stdio` | `command` | `env` | `args` may provide command arguments. | +| `sse` | `url` | `headers` | Uses an SSE endpoint. | +| `streamable-http` | `url` | `headers` | The CLI also accepts `http` as an alias and stores `streamable-http`. | + +Definitions use these shapes: + +```json +{ "name": "local-tools", "enabled": true, "transport": "stdio", "command": "node", "args": ["server.js"], "env": { "API_KEY": { "secretRef": "sec_...", "scope": "project" } } } +{ "name": "docs-sse", "transport": "sse", "url": "https://example.test/sse", "headers": { "Authorization": { "secretRef": "sec_...", "scope": "global" } } } +{ "name": "docs-http", "transport": "streamable-http", "url": "https://example.test/mcp", "headers": { "Authorization": { "secretRef": "sec_...", "scope": "project" } } } +``` + +Expected outcome: settings validation accepts only the required fields for the selected transport, rejects duplicate server names within one stored settings array, and rejects plaintext sensitive values. + +## Secret references + +Fusion never persists raw MCP environment values, header values, or token-like material in settings. Sensitive maps store only Fusion-managed secret references: + +```json +{ "secretRef": "sec_...", "scope": "project" } +``` + +Use `scope: "project"` for secrets stored in the current project and `scope: "global"` for secrets stored in the global secrets database. The plaintext value lives in the encrypted [Secrets](./secrets.md) store, not in `mcpServers`. + +Fusion materializes MCP secret references only at the use seam: + +- when creating an AI session for an MCP-capable runtime; +- when running a bounded validation/reachability probe; +- when importing plaintext Claude Desktop env/header values and immediately creating Fusion secrets. + +Expected outcome: API responses, CLI output, settings JSON, exports, and structured logs show secret references or counts/status metadata only; they do not include decrypted env/header values. + +## Validation and reachability + +The dashboard **Test** control calls `POST /api/mcp/validate` for one server. The route accepts a JSON body with either: + +- `name` — resolve a configured server by name in the current project context; or +- `server` / `definition` — validate and probe the supplied server definition. + +`timeoutMs` is optional, must be positive, and is capped at 30000 milliseconds. The response is: + +```json +{ "status": "valid", "message": "..." } +``` + +`status` is one of: + +| Status | Meaning | +|---|---| +| `valid` | The definition resolved, secrets materialized, and the bounded probe reached the server. | +| `unreachable` | The definition resolved, but the probe could not reach the server within the bounded check. | +| `error` | Validation, secret resolution, spawn, fetch, or protocol setup failed. | + +Expected outcome: validation returns only `{ status, message? }`; resolved `env` and `headers` values are never returned. + +Note: `fn mcp validate` currently validates stored definitions and reports whether they satisfy Fusion's schema. It does not perform the dashboard/API reachability probe. + +## Managing servers in the dashboard + +1. Open **Settings → Global → MCP Servers** for shared defaults, or **Settings → Project → MCP Servers** for project-specific servers. Expected outcome: the **Global MCP servers** or **Project MCP servers** card appears. +2. Turn on **Enable MCP servers for this scope**. Expected outcome: the current scope's `mcpServers.enabled` draft becomes true; project scope overrides global enablement when saved. +3. Click **Add server**. Choose `stdio`, `SSE`, or `HTTP`, then enter the required `command` or `url`. Expected outcome: the editor only asks for fields used by that transport and saves `HTTP` as `streamable-http`. +4. Add sensitive values under **Environment secret refs** for `stdio` or **Header secret refs** for `sse` / `streamable-http`. Choose an existing secret or create a new secret with **Create secret**. Expected outcome: the settings draft receives only `{ secretRef, scope }`; the plaintext creation value is stored in Secrets, not in settings. +5. Save the server. Expected outcome: the row appears with its transport, state badge, and validation status of **Not tested**. +6. In project settings, review inherited rows from global settings. Use **Override** to replace an inherited server or **Disable** to add a same-named project disabled entry. Expected outcome: state badges identify inherited, overridden, project-local, and disabled-global behavior before you save. +7. Click **Test** on a server row. Expected outcome: the row shows **Testing…** while pending, then `valid`, `unreachable`, or `error` with the returned message. +8. Use the **Import** pane to paste JSON or choose **Upload JSON**. Expected outcome: Claude Desktop-style servers are added to the draft, duplicate names are rejected, and plaintext env/header values are converted into newly created Fusion secrets plus secret references. +9. Use **Copy Fusion MCP JSON** and then **Download JSON** when needed. Expected outcome: the export contains Fusion MCP JSON with secret references, and no plaintext secret values. +10. Save the Settings modal. Expected outcome: the selected global or project `mcpServers` settings are persisted and used by subsequent MCP-capable AI sessions. + +## Managing servers from the CLI + +1. List configured servers: + + ```bash + fn mcp list [--project ] [--json] + ``` + + Expected outcome: Fusion prints global, project, and effective servers with secret summaries such as `project secret`, never decrypted values. + +2. Add a stdio server: + + ```bash + fn mcp add local-tools --scope project --transport stdio --command node --arg server.js --env API_KEY=my-existing-secret --secret-scope project + ``` + + Expected outcome: Fusion resolves `my-existing-secret` by id or key, stores it as `{ secretRef, scope }`, and prints `✓ Added MCP server "local-tools" to project scope`. + +3. Add an SSE or streamable HTTP server: + + ```bash + fn mcp add docs --scope global --transport sse --url https://example.test/sse --header Authorization=docs-token --secret-scope global + fn mcp add http-docs --scope project --transport http --url https://example.test/mcp --secret-ref docs-token --secret-scope project + ``` + + Expected outcome: `sse` stores an SSE server, `http` is normalized to `streamable-http`, and `--secret-ref` supplies a token-like default secret field when no explicit `--env` or `--header` is present. + +4. Create secrets while adding or editing: + + ```bash + fn mcp add private-docs --scope project --transport streamable-http --url https://example.test/mcp --create-secret-header Authorization=Bearer-token-value + ``` + + Expected outcome: the CLI creates a Fusion secret with a suggested MCP key and persists only the new secret reference in settings. + +5. Edit a scoped server: + + ```bash + fn mcp edit local-tools --scope project --command node --args '["server.js","--verbose"]' + ``` + + Expected outcome: only the selected global or project declaration changes; effective project-over-global behavior is recomputed later by name. + +6. Enable or disable a server: + + ```bash + fn mcp enable local-tools --scope project + fn mcp disable local-tools --scope project + ``` + + Expected outcome: Fusion flips the selected declaration's `enabled` flag. At project scope, disabling a same-named inherited server masks the global declaration without deleting it. + +7. Remove a scoped declaration: + + ```bash + fn mcp remove local-tools --scope project + ``` + + Expected outcome: the selected declaration is deleted. If you remove a project override, a same-named global server may become effective again. + +8. Validate stored definitions: + + ```bash + fn mcp validate [--scope global|project|effective] [--json] + ``` + + Expected outcome: Fusion reports whether stored definitions satisfy the MCP settings schema. Use the dashboard **Test** control or `POST /api/mcp/validate` for reachability. + +## Importing Claude Desktop configuration + +1. Prepare a Claude Desktop-style JSON file or paste payload: + + ```json + { + "mcpServers": { + "docs": { + "command": "node", + "args": ["server.js"], + "env": { "API_KEY": "plaintext-from-claude-config" } + } + } + } + ``` + + Expected outcome: Fusion recognizes the `mcpServers` object and maps each entry to a named MCP server. + +2. Import from the dashboard by opening **Settings → Global → MCP Servers** or **Settings → Project → MCP Servers**, pasting the JSON into **Import**, or choosing **Upload JSON**, then clicking **Import**. Expected outcome: plaintext env/header values are converted into Fusion secrets with `prompt` access policy and settings receive only secret references. + +3. Import from the CLI: + + ```bash + fn mcp import ./claude_desktop_config.json --scope project --yes + ``` + + Expected outcome: the CLI prints an import summary, creates Fusion secrets for plaintext env/header values, replaces them with secret references, and imports the definitions into the selected scope. + +4. Review the imported rows with `fn mcp list` or the dashboard card before saving/applying broader settings changes. Expected outcome: duplicate names are visible, secret fields show as references, and effective project-over-global behavior is clear. + +## Exporting Fusion MCP configuration + +1. Export from the dashboard by opening the MCP settings card and clicking **Copy Fusion MCP JSON**. Expected outcome: the JSON is copied when clipboard access is available, and the export text appears for manual copy. +2. Click **Download JSON** after generating the dashboard export. Expected outcome: the browser downloads `fusion-mcp-servers.json`. +3. Export from the CLI: + + ```bash + fn mcp export --scope effective --output fusion-mcp-servers.json + fn mcp export --scope global --json + ``` + + Expected outcome: `--output` writes the JSON to a file; without `--output`, Fusion prints JSON to stdout. `global`, `project`, and `effective` choose stored global declarations, stored project declarations, or resolved project-over-global output. +4. Inspect the exported secret fields. Expected outcome: env/header values remain `{ secretRef, scope }` references and are not decrypted into plaintext. + +## How MCP servers reach AI lanes + +When an AI lane starts a session, Fusion resolves the effective `mcpServers` settings, materializes secret references through the scoped secrets store, and passes the resulting in-memory server declarations to runtimes that support MCP. The forwarding path covers chat/planning, executor, reviewer, validator, merger, workflow model nodes, summarization, evaluator, research, cron/automation, mission, and reflection paths. + +Runtime support is guarded. Claude/pi/ACP-compatible runtimes receive MCP servers; mock or unsupported runtimes skip forwarding and emit only structured count/provider/runtime metadata. Skipped forwarding is not a settings error: it means the selected runtime does not accept MCP server declarations. + +Expected outcome: enabling a server makes it available to subsequent supported AI sessions, while unsupported sessions continue without MCP tools and without logging secret-bearing server definitions. + +See [Settings Reference](./settings-reference.md) for the `mcpServers` settings contract and [Agents](./agents.md) for runtime/model lane behavior. diff --git a/docs/secrets.md b/docs/secrets.md index 256bb7fa2c..e6502abd3e 100644 --- a/docs/secrets.md +++ b/docs/secrets.md @@ -41,7 +41,7 @@ Threat-model baseline: - MCP server settings store only secret references for sensitive env/header/token fields; imports surface plaintext as secret-creation descriptors instead of persisting it in settings. - MCP server secret references are materialized only at session/probe creation time for MCP-capable AI lanes and `POST /api/mcp/validate`; responses and structured logs include status/count metadata only, never resolved env/header values. -See also: [Storage](./storage.md), [Multi-project](./multi-project.md), [Architecture](./architecture.md), [Settings reference](./settings-reference.md). +See also: [Storage](./storage.md), [Multi-project](./multi-project.md), [Architecture](./architecture.md), [Settings reference](./settings-reference.md), and [MCP](./mcp.md) for MCP-specific secret-reference workflows. ## Architecture diff --git a/docs/settings-reference.md b/docs/settings-reference.md index 6399990d86..2e0f6789e7 100644 --- a/docs/settings-reference.md +++ b/docs/settings-reference.md @@ -166,6 +166,8 @@ Secret rule: `env` and `headers` maps are sensitive. Values must be Fusion secre `POST /api/mcp/validate` validates an MCP server definition or configured server name against the current project context. The route resolves and materializes the target server with the same secret rules, then performs a bounded reachability probe (`stdio` supervised spawn, `sse`/`streamable-http` bounded fetch) and returns only `{ status, message? }` without resolved env/header contents. +See [MCP](./mcp.md) for the full configuration and usage guide, including dashboard, CLI, Claude Desktop import, Fusion export, and reachability procedures. + ### Notification providers (pluggable) Fusion now supports a provider-list notification model via `notificationProviders` while keeping legacy flat ntfy/webhook settings intact. diff --git a/packages/dashboard/src/__tests__/mcp-documentation.test.ts b/packages/dashboard/src/__tests__/mcp-documentation.test.ts new file mode 100644 index 0000000000..6f9829284f --- /dev/null +++ b/packages/dashboard/src/__tests__/mcp-documentation.test.ts @@ -0,0 +1,65 @@ +// @vitest-environment node + +import { describe, expect, it } from "vitest"; +import { readFileSync } from "node:fs"; +import path from "node:path"; + +const repoRoot = path.resolve(__dirname, "../../../../"); + +function readDoc(relativePath: string): string { + return readFileSync(path.join(repoRoot, relativePath), "utf8"); +} + +describe("MCP documentation contract", () => { + it("includes the canonical MCP guide and required cross-references", () => { + const mcpGuide = readDoc("docs/mcp.md"); + const docsIndex = readDoc("docs/README.md"); + const settingsReference = readDoc("docs/settings-reference.md"); + const cliReference = readDoc("docs/cli-reference.md"); + const dashboardGuide = readDoc("docs/dashboard-guide.md"); + + expect(mcpGuide).toContain("# MCP (Model Context Protocol)"); + expect(mcpGuide).toContain("## Overview"); + expect(mcpGuide).toContain("## Server definitions and transports"); + expect(mcpGuide).toContain("## Secret references"); + expect(mcpGuide).toContain("## Validation and reachability"); + expect(mcpGuide).toContain("## Managing servers in the dashboard"); + expect(mcpGuide).toContain("## Managing servers from the CLI"); + expect(mcpGuide).toContain("## Importing Claude Desktop configuration"); + expect(mcpGuide).toContain("## Exporting Fusion MCP configuration"); + expect(mcpGuide).toContain("## How MCP servers reach AI lanes"); + + expect(docsIndex).toContain("[MCP](./mcp.md)"); + expect(settingsReference).toContain("[MCP](./mcp.md)"); + expect(cliReference).toContain("[MCP](./mcp.md)"); + expect(dashboardGuide).toContain("[MCP](./mcp.md)"); + }); + + it("keeps documented MCP implementation surfaces aligned with source", () => { + const mcpGuide = readDoc("docs/mcp.md"); + const routeSource = readDoc("packages/dashboard/src/routes.ts"); + const cliSource = readDoc("packages/cli/src/commands/mcp.ts"); + const settingsModalSource = readDoc("packages/dashboard/app/components/SettingsModal.tsx"); + + expect(routeSource).toContain('router.post("/mcp/validate"'); + expect(routeSource).toContain("server?: unknown"); + expect(routeSource).toContain("definition?: unknown"); + expect(routeSource).toContain("timeoutMs?: unknown"); + expect(mcpGuide).toContain("POST /api/mcp/validate"); + expect(mcpGuide).toContain("`valid`"); + expect(mcpGuide).toContain("`unreachable`"); + expect(mcpGuide).toContain("`error`"); + + for (const command of ["runMcpList", "runMcpAdd", "runMcpEdit", "runMcpRemove", "runMcpEnable", "runMcpDisable", "runMcpImport", "runMcpExport", "runMcpValidate"]) { + expect(cliSource).toContain(`export async function ${command}`); + } + for (const flag of ["--scope", "--transport", "--secret-ref", "--secret-scope", "--output", "--json", "--yes"]) { + expect(mcpGuide).toContain(flag); + } + + expect(settingsModalSource).toContain('id: "global-mcp"'); + expect(settingsModalSource).toContain('id: "mcp"'); + expect(mcpGuide).toContain("Settings → Global → MCP Servers"); + expect(mcpGuide).toContain("Settings → Project → MCP Servers"); + }); +});