All checks were successful
Test / rust-fmt-check (pull_request) Successful in 1m24s
Test / frontend-tests (pull_request) Successful in 1m40s
Test / frontend-typecheck (pull_request) Successful in 1m44s
Test / rust-clippy (pull_request) Successful in 3m13s
Test / rust-tests (pull_request) Successful in 4m35s
PR Review Automation / review (pull_request) Successful in 4m41s
307 lines
14 KiB
Markdown
307 lines
14 KiB
Markdown
# 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`). **Migration 023** adds the `env_config` column.
|
|
|
|
### `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 |
|
|
| `env_config` | TEXT | Nullable — AES-256-GCM encrypted JSON object of env vars |
|
|
| `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"],
|
|
"env": {
|
|
"DEBUG": "1",
|
|
"LOG_LEVEL": "info"
|
|
}
|
|
}
|
|
```
|
|
|
|
- `command` — **must be an absolute path**. Relative paths are rejected to prevent path traversal attacks.
|
|
- `args` — optional array of command-line arguments.
|
|
- `env` — optional object of plaintext environment variables for non-sensitive values.
|
|
|
|
Sensitive environment variables (API keys, tokens) are stored separately in the `env_config` column (AES-256-GCM encrypted) and merged with plaintext env vars at discovery time. Encrypted values take precedence over plaintext for duplicate keys.
|
|
|
|
The process is spawned via Tokio and wrapped with `rmcp::transport::TokioChildProcess`.
|
|
|
|
#### Important: PATH for npx/node-based servers
|
|
|
|
When TFTSR spawns a stdio process from a macOS `.app` bundle, it runs in a **stripped environment** — the system `PATH` is not inherited. Any server that relies on `node`, `npx`, `python`, or other tools found via `PATH` must have it explicitly set.
|
|
|
|
In the **Environment Variables (Plaintext)** field, add:
|
|
|
|
```
|
|
PATH=/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin
|
|
```
|
|
|
|
Adjust the paths to match where your runtime is installed (`which node` will show the correct directory).
|
|
|
|
**Example: GitHub MCP server**
|
|
|
|
| Field | Value |
|
|
|-------|-------|
|
|
| Transport | stdio |
|
|
| Command | `/opt/homebrew/bin/npx` |
|
|
| Arguments | `-y @modelcontextprotocol/server-github` |
|
|
| Auth Type | none |
|
|
| Environment Variables (Plaintext) | `PATH=/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin` |
|
|
| Secure Environment Variables (Encrypted) | `GITHUB_PERSONAL_ACCESS_TOKEN=ghp_yourtoken` |
|
|
|
|
### 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: leave blank.
|
|
- **Transport** — `stdio` or `http`.
|
|
- **Command** (stdio only) — Absolute path to the executable (e.g., `/opt/homebrew/bin/npx`).
|
|
- **Arguments** (stdio only) — Space-separated arguments (e.g., `-y @modelcontextprotocol/server-github`).
|
|
- **Auth Type** — `none`, `api_key`, `bearer`, or `oauth2`.
|
|
- **Auth Value** — The token/key (will be encrypted on save). Leave blank for `none`.
|
|
- **Environment Variables (Plaintext)** (stdio only) — Space-separated `KEY=value` pairs for non-sensitive values. **Always include `PATH=...` for `npx`/node/python-based servers** — the app bundle does not inherit the system PATH.
|
|
- **Secure Environment Variables (Encrypted)** (stdio only) — Space-separated `KEY=value` pairs for sensitive values (API keys, tokens). Stored AES-256-GCM encrypted. Leave blank when editing to preserve existing values.
|
|
- **Custom Headers** (HTTP only) — Space-separated `KEY:value` pairs for custom HTTP headers.
|
|
- **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<String, Arc<TokioMutex<McpConnection>>>>`).
|
|
- 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
|
|
- **Encrypted env vars** — sensitive environment variables stored AES-256-GCM encrypted in `env_config`; never returned to the frontend
|
|
- **Dangerous env var blocking** — `LD_PRELOAD`, `LD_LIBRARY_PATH`, `DYLD_INSERT_LIBRARIES`, and related privilege-escalation variables are rejected at the transport layer
|
|
- **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.
|