Merge pull request 'fix(mcp): add environment variable support for stdio MCP servers' (#62) from bug/mcp-env-vars-support into master
All checks were successful
Auto Tag / autotag (push) Successful in 7s
Auto Tag / wiki-sync (push) Successful in 6s
Test / rust-fmt-check (push) Successful in 1m18s
Test / frontend-typecheck (push) Successful in 1m32s
Test / frontend-tests (push) Successful in 1m31s
Auto Tag / changelog (push) Successful in 1m29s
Test / rust-clippy (push) Successful in 3m52s
Test / rust-tests (push) Successful in 5m28s
Auto Tag / build-macos-arm64 (push) Successful in 5m46s
Auto Tag / build-linux-amd64 (push) Successful in 9m28s
Auto Tag / build-windows-amd64 (push) Successful in 11m20s
Auto Tag / build-linux-arm64 (push) Successful in 11m31s
All checks were successful
Auto Tag / autotag (push) Successful in 7s
Auto Tag / wiki-sync (push) Successful in 6s
Test / rust-fmt-check (push) Successful in 1m18s
Test / frontend-typecheck (push) Successful in 1m32s
Test / frontend-tests (push) Successful in 1m31s
Auto Tag / changelog (push) Successful in 1m29s
Test / rust-clippy (push) Successful in 3m52s
Test / rust-tests (push) Successful in 5m28s
Auto Tag / build-macos-arm64 (push) Successful in 5m46s
Auto Tag / build-linux-amd64 (push) Successful in 9m28s
Auto Tag / build-windows-amd64 (push) Successful in 11m20s
Auto Tag / build-linux-arm64 (push) Successful in 11m31s
Reviewed-on: #62
This commit is contained in:
commit
590fec7dd4
@ -53,7 +53,7 @@ MCP support extends the AI's capabilities beyond conversation: during incident t
|
|||||||
|
|
||||||
## Database Schema
|
## Database Schema
|
||||||
|
|
||||||
Three tables are created by **Migration 018** (`018_mcp_servers`):
|
Three tables are created by **Migration 018** (`018_mcp_servers`). **Migration 023** adds the `env_config` column.
|
||||||
|
|
||||||
### `mcp_servers`
|
### `mcp_servers`
|
||||||
|
|
||||||
@ -66,6 +66,7 @@ Three tables are created by **Migration 018** (`018_mcp_servers`):
|
|||||||
| `transport_config` | TEXT | NOT NULL DEFAULT `'{}'` (JSON) |
|
| `transport_config` | TEXT | NOT NULL DEFAULT `'{}'` (JSON) |
|
||||||
| `auth_type` | TEXT | NOT NULL, CHECK IN (`'none'`, `'api_key'`, `'bearer'`, `'oauth2'`) |
|
| `auth_type` | TEXT | NOT NULL, CHECK IN (`'none'`, `'api_key'`, `'bearer'`, `'oauth2'`) |
|
||||||
| `auth_value` | TEXT | Nullable — AES-256-GCM encrypted |
|
| `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 |
|
| `enabled` | INTEGER | NOT NULL DEFAULT 1 |
|
||||||
| `last_discovered_at` | TEXT | Nullable UTC timestamp |
|
| `last_discovered_at` | TEXT | Nullable UTC timestamp |
|
||||||
| `discovery_status` | TEXT | NOT NULL DEFAULT `'pending'`, CHECK IN (`'pending'`, `'connected'`, `'unreachable'`, `'error'`) |
|
| `discovery_status` | TEXT | NOT NULL DEFAULT `'pending'`, CHECK IN (`'pending'`, `'connected'`, `'unreachable'`, `'error'`) |
|
||||||
@ -108,15 +109,45 @@ The app spawns a local process and communicates over its stdin/stdout using the
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"command": "/usr/local/bin/my-mcp-server",
|
"command": "/usr/local/bin/my-mcp-server",
|
||||||
"args": ["--port", "0", "--mode", "stdio"]
|
"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.
|
- `command` — **must be an absolute path**. Relative paths are rejected to prevent path traversal attacks.
|
||||||
- `args` — optional array of command-line arguments.
|
- `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`.
|
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)
|
### http (Streamable HTTP)
|
||||||
|
|
||||||
The app connects to a remote MCP server over HTTP(S) using the Streamable HTTP transport from `rmcp`.
|
The app connects to a remote MCP server over HTTP(S) using the Streamable HTTP transport from `rmcp`.
|
||||||
@ -169,11 +200,15 @@ Navigate to **Settings > MCP Servers** (`/settings/mcp`) to manage servers.
|
|||||||
1. Click **Add Server**.
|
1. Click **Add Server**.
|
||||||
2. Fill in:
|
2. Fill in:
|
||||||
- **Name** — Human-readable label (e.g., "Weather API", "Filesystem Tools").
|
- **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.
|
- **URL** — For HTTP: the server endpoint. For stdio: leave blank.
|
||||||
- **Transport** — `stdio` or `http`.
|
- **Transport** — `stdio` or `http`.
|
||||||
- **Transport Config** — JSON. For stdio: `{"command": "/path/to/binary", "args": [...]}`. For HTTP: typically `{}`.
|
- **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 Type** — `none`, `api_key`, `bearer`, or `oauth2`.
|
||||||
- **Auth Value** — The token/key (will be encrypted on save). Leave blank for `none`.
|
- **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.
|
- **Enabled** — Toggle on/off.
|
||||||
3. Click **Save**. The server record is persisted.
|
3. Click **Save**. The server record is persisted.
|
||||||
4. Click **Discover** to connect and enumerate available tools and resources.
|
4. Click **Discover** to connect and enumerate available tools and resources.
|
||||||
@ -263,6 +298,8 @@ See [IPC Commands](IPC-Commands#mcp-servers) for full type signatures.
|
|||||||
- **Audit logging** — `write_audit_event` called before every MCP tool execution
|
- **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)
|
- **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
|
- **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
|
- **Cascade deletes** — Removing a server removes all associated tools and resources
|
||||||
- **TLS** — HTTP transport uses `reqwest` with certificate verification for HTTPS endpoints
|
- **TLS** — HTTP transport uses `reqwest` with certificate verification for HTTPS endpoints
|
||||||
|
|
||||||
|
|||||||
@ -103,9 +103,16 @@ async fn discover_server_inner(
|
|||||||
other => return Err(format!("Unknown transport type: {other}")),
|
other => return Err(format!("Unknown transport type: {other}")),
|
||||||
};
|
};
|
||||||
|
|
||||||
// List tools and resources
|
// List tools and resources (resources are optional — not all servers implement them)
|
||||||
let tools = list_tools(&conn, &server.id, &server.name).await?;
|
let tools = list_tools(&conn, &server.id, &server.name).await?;
|
||||||
let resources = list_resources(&conn, &server.id).await?;
|
let resources = match list_resources(&conn, &server.id).await {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(e) if e.contains("-32601") || e.contains("Method not found") => {
|
||||||
|
info!(server_id = %server.id, "Server does not support resources/list (optional)");
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
Err(e) => return Err(e),
|
||||||
|
};
|
||||||
|
|
||||||
// Persist to DB
|
// Persist to DB
|
||||||
{
|
{
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user