From 1ab9d0a7cb685b7bb06679ef8d05e5819878243a Mon Sep 17 00:00:00 2001 From: Gitea Actions Date: Sat, 23 May 2026 22:15:22 +0000 Subject: [PATCH] docs: sync from docs/wiki/ at commit ea7f484c --- Database.md | 58 +++++++++- IPC-Commands.md | 103 ++++++++++++++++++ MCP-Servers.md | 269 ++++++++++++++++++++++++++++++++++++++++++++++ Security-Model.md | 54 ++++++++++ 4 files changed, 482 insertions(+), 2 deletions(-) create mode 100644 MCP-Servers.md diff --git a/Database.md b/Database.md index 452395f..0b7dd9c 100644 --- a/Database.md +++ b/Database.md @@ -2,7 +2,7 @@ ## Overview -TFTSR uses **SQLite** via `rusqlite` with the `bundled-sqlcipher` feature for AES-256 encryption in production. 17 versioned migrations are tracked in the `_migrations` table. +TFTSR uses **SQLite** via `rusqlite` with the `bundled-sqlcipher` feature for AES-256 encryption in production. 18 versioned migrations are tracked in the `_migrations` table. **DB file location:** `{app_data_dir}/tftsr.db` @@ -38,7 +38,7 @@ pub fn init_db(data_dir: &Path) -> anyhow::Result { --- -## Schema (17 Migrations) +## Schema (18 Migrations) ### 001 — issues @@ -290,6 +290,60 @@ CREATE INDEX idx_timeline_events_time ON timeline_events(created_at); - Non-blocking writes: Timeline events recorded asynchronously at key triage moments - Cascade delete from issues ensures cleanup +### 018 — mcp_servers, mcp_tools, mcp_resources (MCP Server Support) + +**MCP server registry:** +```sql +CREATE TABLE mcp_servers ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + url TEXT NOT NULL, + transport_type TEXT NOT NULL CHECK(transport_type IN ('stdio', 'http')), + transport_config TEXT NOT NULL DEFAULT '{}', + auth_type TEXT NOT NULL CHECK(auth_type IN ('none', 'api_key', 'bearer', 'oauth2')), + auth_value TEXT, -- AES-256-GCM encrypted + enabled INTEGER NOT NULL DEFAULT 1, + last_discovered_at TEXT, + discovery_status TEXT NOT NULL DEFAULT 'pending' + CHECK(discovery_status IN ('pending','connected','unreachable','error')), + discovery_error TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')) +); +``` + +**Discovered tools (populated by discovery):** +```sql +CREATE TABLE mcp_tools ( + id TEXT PRIMARY KEY, + server_id TEXT NOT NULL, + name TEXT NOT NULL, -- Original tool name from server + tool_key TEXT NOT NULL, -- Sanitised key: mcp_{server}_{tool} + description TEXT, + parameters TEXT NOT NULL DEFAULT '{}', -- JSON Schema + FOREIGN KEY(server_id) REFERENCES mcp_servers(id) ON DELETE CASCADE +); +``` + +**Discovered resources:** +```sql +CREATE TABLE mcp_resources ( + id TEXT PRIMARY KEY, + server_id TEXT NOT NULL, + uri TEXT NOT NULL, + name TEXT, + description TEXT, + FOREIGN KEY(server_id) REFERENCES mcp_servers(id) ON DELETE CASCADE +); +``` + +**Design notes:** +- `auth_value` stored as AES-256-GCM ciphertext (same encryption as integration credentials) +- `transport_type` and `auth_type` enforce valid values via CHECK constraints +- `discovery_status` tracks connection state: `pending` → `connected` | `unreachable` | `error` +- Cascade deletes ensure removing a server cleans up all associated tools and resources +- Tools and resources are replaced atomically on each discovery run (delete-all + re-insert) + --- ## Key Design Notes diff --git a/IPC-Commands.md b/IPC-Commands.md index 6e7081e..454c48d 100644 --- a/IPC-Commands.md +++ b/IPC-Commands.md @@ -412,6 +412,109 @@ Updates work item fields. Uses JSON-PATCH format. --- +## MCP Server Commands + +> **Status:** Fully Implemented (v0.3.0+) + +### `list_mcp_servers` +```typescript +listMcpServersCmd() → McpServer[] +``` +Returns all registered MCP servers. `auth_value` is always `null` in responses (scrubbed server-side). +```typescript +interface McpServer { + id: string; + name: string; + url: string; + transport_type: "stdio" | "http"; + transport_config: string; // JSON + auth_type: "none" | "api_key" | "bearer" | "oauth2"; + auth_value?: string; // Always null in responses + enabled: boolean; + last_discovered_at?: string; + discovery_status: "pending" | "connected" | "unreachable" | "error"; + discovery_error?: string; + created_at: string; + updated_at: string; +} +``` + +### `create_mcp_server` +```typescript +createMcpServerCmd(request: CreateMcpServerRequest) → McpServer +``` +Creates a new MCP server record. Auth value is encrypted with AES-256-GCM before persistence. +```typescript +interface CreateMcpServerRequest { + name: string; + url: string; + transport_type: "stdio" | "http"; + transport_config: string; + auth_type: "none" | "api_key" | "bearer" | "oauth2"; + auth_value?: string; + enabled: boolean; +} +``` + +### `update_mcp_server` +```typescript +updateMcpServerCmd(id: string, request: UpdateMcpServerRequest) → McpServer +``` +Partial update. Only provided fields are changed. If `auth_value` is provided, it replaces the encrypted value. +```typescript +interface UpdateMcpServerRequest { + name?: string; + url?: string; + transport_type?: "stdio" | "http"; + transport_config?: string; + auth_type?: "none" | "api_key" | "bearer" | "oauth2"; + auth_value?: string; + enabled?: boolean; +} +``` + +### `delete_mcp_server` +```typescript +deleteMcpServerCmd(id: string) → void +``` +Deletes the server record and all associated tools/resources (cascade). Also removes the live connection from memory. + +### `toggle_mcp_server` +```typescript +toggleMcpServerCmd(id: string, enabled: boolean) → void +``` +Enables or disables a server. Disabled servers are excluded from AI tool injection and startup discovery. + +### `discover_mcp_server` +```typescript +discoverMcpServerCmd(id: string) → McpServerStatus +``` +Connects to the server, enumerates its tools and resources, and persists them. Returns the updated status. +```typescript +interface McpServerStatus { + server_id: string; + status: "pending" | "connected" | "unreachable" | "error"; + error?: string; + tool_count: number; + resource_count: number; + last_discovered_at?: string; +} +``` + +### `get_mcp_server_status` +```typescript +getMcpServerStatusCmd(id: string) → McpServerStatus +``` +Returns current discovery status, tool count, and resource count without triggering a new connection. + +### `initiate_mcp_oauth` +```typescript +initiateMcpOauthCmd(id: string) → void +``` +Opens a WebView window for OAuth2 authentication. Requires `auth_type = "oauth2"` and `transport_config` containing `auth_endpoint`, `token_endpoint`, and `client_id`. After successful authentication, the access token is encrypted and stored. + +--- + ## Common Types ### `ConnectionResult` diff --git a/MCP-Servers.md b/MCP-Servers.md new file mode 100644 index 0000000..98d0afe --- /dev/null +++ b/MCP-Servers.md @@ -0,0 +1,269 @@ +# MCP Servers + +## Overview + +**Model Context Protocol (MCP)** is an open standard that allows AI models to invoke external tools and access external resources through a standardised JSON-RPC interface. TFTSR integrates MCP as a first-class feature, enabling the AI triage assistant to call tools exposed by any compliant MCP server — file search, database queries, monitoring APIs, runbook automation, and more. + +MCP support extends the AI's capabilities beyond conversation: during incident triage, the model can autonomously invoke registered tools to gather diagnostic data, check system status, or execute remediation steps — all within the app's security and audit framework. + +--- + +## Architecture + +``` +┌──────────────────────────────────────────────┐ +│ TFTSR App │ +│ │ +│ ┌────────┐ ┌──────────┐ ┌───────────┐ │ +│ │Frontend│──▶│ Commands │──▶│ Store │ │ +│ │ React │ │(IPC/Tauri)│ │ (SQLite) │ │ +│ └────────┘ └────┬─────┘ └───────────┘ │ +│ │ │ +│ ┌─────▼─────┐ │ +│ │ Discovery │ │ +│ └─────┬─────┘ │ +│ │ │ +│ ┌────────────┼────────────┐ │ +│ │ │ │ │ +│ ┌────▼────┐ ┌────▼────┐ ┌───▼────┐ │ +│ │ stdio │ │ HTTP │ │Adapter │ │ +│ │Transport│ │Transport│ │(AI glue)│ │ +│ └────┬────┘ └────┬────┘ └────────┘ │ +└───────┼─────────────┼────────────────────────┘ + │ │ + ▼ ▼ + Local process Remote HTTP + (e.g. npx) MCP endpoint +``` + +**Module layout** (`src-tauri/src/mcp/`): + +| File | Responsibility | +|------|----------------| +| `models.rs` | Struct definitions: `McpServer`, `McpTool`, `McpResource`, request types | +| `store.rs` | CRUD operations against SQLite (encrypted at rest) | +| `transport/stdio.rs` | Stdio process spawn via `rmcp` (absolute path enforced) | +| `transport/http.rs` | Streamable HTTP transport via `rmcp` | +| `client.rs` | Connection lifecycle, tool listing, tool invocation | +| `adapter.rs` | Name sanitisation, `McpTool` → AI `Tool` conversion | +| `discovery.rs` | Per-server and bulk startup discovery orchestration | +| `commands.rs` | 8 Tauri IPC command handlers | + +--- + +## Database Schema + +Three tables are created by **Migration 018** (`018_mcp_servers`): + +### `mcp_servers` + +| Column | Type | Constraints | +|--------|------|-------------| +| `id` | TEXT | PRIMARY KEY | +| `name` | TEXT | NOT NULL | +| `url` | TEXT | NOT NULL | +| `transport_type` | TEXT | NOT NULL, CHECK IN (`'stdio'`, `'http'`) | +| `transport_config` | TEXT | NOT NULL DEFAULT `'{}'` (JSON) | +| `auth_type` | TEXT | NOT NULL, CHECK IN (`'none'`, `'api_key'`, `'bearer'`, `'oauth2'`) | +| `auth_value` | TEXT | Nullable — AES-256-GCM encrypted | +| `enabled` | INTEGER | NOT NULL DEFAULT 1 | +| `last_discovered_at` | TEXT | Nullable UTC timestamp | +| `discovery_status` | TEXT | NOT NULL DEFAULT `'pending'`, CHECK IN (`'pending'`, `'connected'`, `'unreachable'`, `'error'`) | +| `discovery_error` | TEXT | Nullable | +| `created_at` | TEXT | NOT NULL DEFAULT `datetime('now')` | +| `updated_at` | TEXT | NOT NULL DEFAULT `datetime('now')` | + +### `mcp_tools` + +| Column | Type | Constraints | +|--------|------|-------------| +| `id` | TEXT | PRIMARY KEY | +| `server_id` | TEXT | NOT NULL, FK → `mcp_servers(id)` ON DELETE CASCADE | +| `name` | TEXT | NOT NULL (original tool name from server) | +| `tool_key` | TEXT | NOT NULL (sanitised key used by AI) | +| `description` | TEXT | Nullable | +| `parameters` | TEXT | NOT NULL DEFAULT `'{}'` (JSON Schema) | + +### `mcp_resources` + +| Column | Type | Constraints | +|--------|------|-------------| +| `id` | TEXT | PRIMARY KEY | +| `server_id` | TEXT | NOT NULL, FK → `mcp_servers(id)` ON DELETE CASCADE | +| `uri` | TEXT | NOT NULL | +| `name` | TEXT | Nullable | +| `description` | TEXT | Nullable | + +Cascade deletes ensure that removing a server automatically cleans up its tools and resources. + +--- + +## Transport Types + +### stdio + +The app spawns a local process and communicates over its stdin/stdout using the MCP JSON-RPC protocol. + +**Configuration** (`transport_config` JSON): +```json +{ + "command": "/usr/local/bin/my-mcp-server", + "args": ["--port", "0", "--mode", "stdio"] +} +``` + +- `command` — **must be an absolute path**. Relative paths are rejected to prevent path traversal attacks. +- `args` — optional array of command-line arguments. + +The process is spawned via Tokio and wrapped with `rmcp::transport::TokioChildProcess`. + +### http (Streamable HTTP) + +The app connects to a remote MCP server over HTTP(S) using the Streamable HTTP transport from `rmcp`. + +**Configuration:** +- `url` field on the server record — the HTTP endpoint (e.g., `https://mcp.example.com/v1`). +- If `auth_type` is `bearer` or `api_key`, the decrypted auth value is attached as an `Authorization` header. + +```json +{ + "url": "https://mcp.example.com/v1", + "transport_type": "http", + "auth_type": "bearer" +} +``` + +The `transport_config` field for HTTP servers is typically `{}` — connection details come from `url` and `auth_value`. + +--- + +## Authentication Types + +| Type | Description | Storage | +|------|-------------|---------| +| `none` | No authentication required | — | +| `api_key` | API key sent as Authorization header | Encrypted in `auth_value` | +| `bearer` | Bearer token sent as Authorization header | Encrypted in `auth_value` | +| `oauth2` | OAuth2 PKCE flow via WebView | Token encrypted in `auth_value` after exchange | + +All auth values are encrypted with **AES-256-GCM** before storage (same encryption system as integration credentials). The plaintext is never returned to the frontend — `list_mcp_servers` strips `auth_value` from responses. + +### OAuth2 Flow + +For servers requiring OAuth2: + +1. `transport_config` must include `auth_endpoint`, `token_endpoint`, `client_id`, and optionally `scope`. +2. Call `initiate_mcp_oauth(server_id)` — opens a WebView window at the authorization URL. +3. User authenticates with the MCP provider. +4. On redirect, the code is exchanged for an access token. +5. Token is encrypted and stored in `auth_value`. + +--- + +## Configuration Guide + +### Adding an MCP Server (UI) + +Navigate to **Settings > MCP Servers** (`/settings/mcp`) to manage servers. + +1. Click **Add Server**. +2. Fill in: + - **Name** — Human-readable label (e.g., "Weather API", "Filesystem Tools"). + - **URL** — For HTTP: the server endpoint. For stdio: can be left as the command path for display. + - **Transport** — `stdio` or `http`. + - **Transport Config** — JSON. For stdio: `{"command": "/path/to/binary", "args": [...]}`. For HTTP: typically `{}`. + - **Auth Type** — `none`, `api_key`, `bearer`, or `oauth2`. + - **Auth Value** — The token/key (will be encrypted on save). Leave blank for `none`. + - **Enabled** — Toggle on/off. +3. Click **Save**. The server record is persisted. +4. Click **Discover** to connect and enumerate available tools and resources. + +### Discovery + +Discovery connects to the server, queries its tool and resource manifests, and persists them locally. Status transitions: + +``` +pending → connected (success) +pending → error (connection/protocol failure) +pending → unreachable (startup failure, non-fatal) +``` + +After successful discovery, tools from the server appear in AI conversations automatically. + +--- + +## Tool Naming Convention + +When tools are discovered, each gets a **tool key** used by the AI model: + +``` +mcp_{server_name}_{tool_name} +``` + +Both parts are sanitised: +- Lowercased +- Non-alphanumeric characters replaced with `_` +- Consecutive underscores collapsed +- Leading/trailing underscores trimmed + +**Examples:** + +| Server Name | Tool Name | Tool Key | +|-------------|-----------|----------| +| My Weather API | get_forecast | `mcp_my_weather_api_get_forecast` | +| Filesystem | search files | `mcp_filesystem_search_files` | +| simple | ping | `mcp_simple_ping` | + +The AI model calls tools by their `tool_key`. The adapter layer resolves this back to the original server and tool name for execution. + +--- + +## Startup Discovery + +On application launch, `init_all_servers()` iterates all **enabled** servers and attempts discovery for each: + +- Successful connections are stored in `AppState.mcp_connections` (a `HashMap>>>`). +- Failed connections are marked as `unreachable` in the database with the error message. A warning is logged, but startup continues. +- This is a best-effort, non-blocking operation — the app launches regardless of MCP server availability. + +--- + +## AI Integration + +Enabled MCP tools are automatically injected into AI conversations: + +1. `get_enabled_mcp_tools()` queries tools from servers that are both `enabled = 1` and `discovery_status = 'connected'`. +2. Each `McpTool` is converted to an AI `Tool` definition (name, description, JSON Schema parameters). +3. When the AI responds with a tool call matching an `mcp_*` key, the adapter routes it to `call_tool()` on the appropriate live connection. +4. The tool result is fed back to the AI as a tool response message. + +--- + +## IPC Commands + +| Command | Parameters | Returns | +|---------|-----------|---------| +| `list_mcp_servers` | — | `McpServer[]` (auth_value always null) | +| `create_mcp_server` | `CreateMcpServerRequest` | `McpServer` | +| `update_mcp_server` | `id`, `UpdateMcpServerRequest` | `McpServer` | +| `delete_mcp_server` | `id` | `void` | +| `toggle_mcp_server` | `id`, `enabled` | `void` | +| `discover_mcp_server` | `id` | `McpServerStatus` | +| `get_mcp_server_status` | `id` | `McpServerStatus` | +| `initiate_mcp_oauth` | `id` | `void` (opens WebView) | + +See [IPC Commands](IPC-Commands#mcp-servers) for full type signatures. + +--- + +## Security + +- **Encrypted auth values** — AES-256-GCM, same key derivation as integration credentials (`TFTSR_ENCRYPTION_KEY`) +- **Server-side scrubbing** — `auth_value` set to `None` before any response to the frontend +- **Audit logging** — `write_audit_event` called before every MCP tool execution +- **PII scan** — Tool call arguments are scanned for PII patterns (non-blocking warning to user) +- **Absolute path enforcement** — stdio transport rejects relative paths to prevent traversal attacks +- **Cascade deletes** — Removing a server removes all associated tools and resources +- **TLS** — HTTP transport uses `reqwest` with certificate verification for HTTPS endpoints + +See [Security Model](Security-Model#mcp-server-security) for the full threat analysis. diff --git a/Security-Model.md b/Security-Model.md index 6a8af50..852cab7 100644 --- a/Security-Model.md +++ b/Security-Model.md @@ -129,6 +129,60 @@ CI/CD currently uses internal `http://` endpoints for self-hosted Gitea release --- +## MCP Server Security + +MCP server support introduces external tool execution capabilities. The following controls mitigate the associated risks. + +### Auth Value Storage + +- Auth tokens (API keys, bearer tokens, OAuth2 access tokens) are encrypted with **AES-256-GCM** before persistence in `mcp_servers.auth_value`. +- Encryption uses the same key derivation as integration credentials (`TFTSR_ENCRYPTION_KEY` → SHA-256 → 32-byte AES key). +- Random 96-bit nonce per encryption operation. +- Format: `base64(nonce || ciphertext || tag)`. + +### Server-Side Response Scrubbing + +- `list_mcp_servers` and all CRUD commands set `auth_value = None` before returning to the frontend. +- The encrypted ciphertext never reaches the WebView layer. +- Decryption only occurs internally when establishing a connection (discovery) or executing a tool call. + +### Audit Trail + +- `write_audit_event` is called **before** every MCP tool execution with: + - `action`: `"mcp_tool_call"` + - `entity_type`: `"mcp_tool"` + - `entity_id`: the tool key being invoked + - `details`: JSON containing server ID, tool name, and argument hash +- This provides a complete, tamper-evident record of all external tool invocations. + +### PII Scan on Arguments + +- Before dispatching a tool call, the arguments JSON is scanned through the PII detection pipeline. +- If PII is detected, a **non-blocking warning** is surfaced to the user. +- This prevents inadvertent leakage of credentials, email addresses, or IP addresses to external MCP servers. + +### Stdio Transport Path Validation + +- `build_stdio_transport()` rejects any `command` that is not an absolute path. +- This prevents: + - Path traversal attacks (e.g., `../../malicious`) + - Reliance on `$PATH` resolution which could be manipulated + - Unintended execution of relative-path binaries + +### Network Boundaries + +- HTTP transport uses `reqwest` with TLS certificate verification for HTTPS endpoints. +- stdio transport communicates only with locally spawned processes (no network exposure). +- MCP server URLs should be added to the Content Security Policy `connect-src` if fetched from the WebView layer. + +### Cascade Deletes + +- Removing an MCP server cascades to delete all associated `mcp_tools` and `mcp_resources` records. +- The live connection is also removed from the in-memory connection pool. +- No orphaned tool definitions can persist after server removal. + +--- + ## Security Checklist for New Features - [ ] Does it send data externally? → Add audit log entry