merge: resolve conflicts with main — keep both DeepResearch tools + MCP functions

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jon Saad-Falcon
2026-03-27 13:09:12 -07:00
co-authored by Claude Opus 4.6
108 changed files with 9316 additions and 2046 deletions
+421
View File
@@ -0,0 +1,421 @@
---
title: macOS Installation Guide
description: Complete step-by-step guide to installing OpenJarvis on macOS with llama.cpp, including common pitfalls and fixes
search:
boost: 2
---
# macOS Installation Guide
This guide walks through a complete OpenJarvis installation on macOS using **llama.cpp** as
the inference engine. It covers every step from scratch — including pitfalls not documented
elsewhere — and is suitable for both Apple Silicon and Intel Macs.
!!! tip "Prefer Ollama?"
If you want the fastest possible setup, use [Ollama](installation.md#ollama-recommended)
instead. This guide is for users who want to run GGUF models directly with llama.cpp,
or who want a deeper understanding of the full stack.
---
## What You'll Install
| Tool | Purpose |
|------|---------|
| Homebrew | macOS package manager — installs everything else |
| uv | Python version and dependency manager |
| Git | Clones the OpenJarvis repo |
| Node.js | Required for the browser UI |
| Rust | Compiles the OpenJarvis security and memory extension |
| llama.cpp | Local inference engine that runs GGUF model files |
| OpenJarvis | The framework itself |
| A GGUF model | The actual AI model (downloaded separately) |
---
## Step-by-Step Installation
### Step 1 — Install Homebrew
Homebrew is the standard macOS package manager. Everything else in this guide is installed
through it.
```bash
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
```
If you already have Homebrew, skip this step.
---
### Step 2 — Install uv
`uv` replaces pip, virtualenv, and pyenv in one tool. OpenJarvis uses it to manage Python
versions, virtual environments, and project dependencies.
```bash
brew install uv
```
---
### Step 3 — Install Git
Git is used to clone the OpenJarvis source code. It may already be present if you have
Xcode Command Line Tools installed.
```bash
brew install git
```
---
### Step 4 — Install Node.js
Node.js is required to build and run the browser frontend. Without it you can still use
the CLI, but not the web UI.
```bash
brew install node
```
---
### Step 5 — Install Rust
OpenJarvis includes a Rust extension that provides security scanning, memory indexing,
rate limiting, and tool execution. It must be compiled from source.
```bash
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
```
After the installer finishes, reload your shell so `rustc` is available:
```bash
source "$HOME/.cargo/env"
```
Verify:
```bash
rustc --version
```
---
### Step 6 — Install llama.cpp
llama.cpp is the inference engine that loads and runs GGUF model files. It is not a model
itself — think of it as a media player and the `.gguf` file as the content.
```bash
brew install llama.cpp
```
---
### Step 7 — Clone the OpenJarvis repo
Run this from your home directory or any neutral parent folder.
```bash
cd ~
git clone https://github.com/open-jarvis/OpenJarvis.git
cd OpenJarvis
```
!!! warning "Do not clone from inside an existing OpenJarvis folder"
A common mistake is running `git clone` while already inside the repo, creating deeply
nested duplicates (`OpenJarvis/OpenJarvis/OpenJarvis`). Always clone from `~` or a
neutral parent directory.
---
### Step 8 — Pin Python to 3.12
!!! warning "Critical step — do not skip"
OpenJarvis requires Python 3.103.13. Its Rust extension uses PyO3, which does not yet
support Python 3.14. If `uv` has Python 3.14 available, it will use it by default,
causing the Rust extension build to fail silently and resulting in ~250 test failures
with `ModuleNotFoundError: No module named 'openjarvis_rust'`.
Pin the project to Python 3.12:
```bash
echo "3.12" > .python-version
uv python install 3.12
rm -rf .venv
uv venv
```
**Restart your terminal**, then verify:
```bash
uv run python --version
# Must show: Python 3.12.x
```
!!! tip "Why restart the terminal?"
Without restarting, the shell may still reference the old virtual environment. This is
the most common reason the version pin appears not to work.
---
### Step 9 — Install Python dependencies
```bash
uv sync --extra dev --extra server
```
The `--extra server` flag adds the FastAPI backend required for the browser UI.
---
### Step 10 — Build the Rust extension
This compiles the Rust extension and installs it into the virtual environment. It provides
security scanning, memory indexing, MCP tool execution, and rate limiting. This step takes
a few minutes on first run.
```bash
uv run maturin develop -m rust/crates/openjarvis-python/Cargo.toml
```
Verify it built correctly:
```bash
uv run python -c "import openjarvis_rust; print('Rust extension OK')"
```
---
### Step 11 — Install frontend dependencies
```bash
cd frontend && npm install && cd ..
```
---
### Step 12 — Download a model
OpenJarvis needs a GGUF model file to run inference. First install the Hugging Face CLI,
then download your chosen model.
```bash
uv tool install huggingface_hub
```
!!! note "The CLI command is `hf`, not `huggingface-cli`"
When installed via `uv tool`, the Hugging Face CLI is invoked as `hf`.
=== "Qwen3 4B (~2.5 GB)"
Faster, lower RAM requirement. Good for most everyday tasks.
```bash
hf download bartowski/Qwen_Qwen3-4B-GGUF \
--include "Qwen_Qwen3-4B-Q4_K_M.gguf" \
--local-dir ~/models
```
=== "Qwen3 8B (~4.7 GB)"
Better reasoning and instruction following. Requires more RAM.
```bash
hf download bartowski/Qwen_Qwen3-8B-GGUF \
--include "Qwen_Qwen3-8B-Q4_K_M.gguf" \
--local-dir ~/models
```
!!! warning "Use the `Qwen_` prefix"
bartowski's Qwen3 repos use the `Qwen_` prefix (e.g. `Qwen_Qwen3-4B-GGUF`). Using
the shorter name without the prefix returns a "repository not found" error.
!!! tip "Apple Silicon vs Intel"
On Apple Silicon, both models benefit from Metal GPU acceleration when using the MLX
engine. On Intel, inference runs on CPU — the 4B model is recommended for speed.
---
### Step 13 — Configure OpenJarvis
Run the init command to detect your hardware and generate a config file:
```bash
uv run jarvis init
```
Then open the config and set the default model to match the filename you downloaded:
```bash
nano ~/.openjarvis/config.toml
```
Find the `default_model` line and update it, for example:
```toml
default_model = "Qwen_Qwen3-4B-Q4_K_M.gguf"
```
---
### Step 14 — Verify the installation
```bash
uv run jarvis doctor
```
A healthy setup looks like this:
```
✓ Python version 3.12.x
✓ Config file ~/.openjarvis/config.toml
✓ Config parsing Config loaded successfully
✓ Engine: llamacpp Reachable
✓ Models: llamacpp Qwen_Qwen3-4B-Q4_K_M.gguf
✓ Default model Qwen_Qwen3-4B-Q4_K_M.gguf (on llamacpp)
```
!!! note "Warnings for other engines are normal"
The `!` warnings for engines like `ollama`, `vllm`, and `lmstudio` simply mean those
backends are not running. You only need `llamacpp` to be reachable.
---
## Running OpenJarvis
### CLI
Start llama-server in one terminal, then run queries in another:
```bash
# Terminal 1 — start the inference engine
llama-server -m ~/models/Qwen_Qwen3-4B-Q4_K_M.gguf -c 4096 -t 8
# Terminal 2 — ask a question
cd ~/OpenJarvis
uv run jarvis ask "What is the capital of France?"
```
### Browser UI
```bash
# Terminal 1 — inference engine
llama-server -m ~/models/Qwen_Qwen3-4B-Q4_K_M.gguf -c 4096 -t 8
# Terminal 2 — backend
cd ~/OpenJarvis && uv run jarvis serve --port 8000
# Terminal 3 — frontend
cd ~/OpenJarvis/frontend && npm run dev
```
Then open [http://localhost:5173](http://localhost:5173).
### Skip typing `uv run` every time
Activate the virtual environment for your current terminal session:
```bash
source ~/OpenJarvis/.venv/bin/activate
```
Your prompt will show `(openjarvis)` when active, and you can type `jarvis ask "..."` directly.
---
## Performance Tips
These tips apply when using llama.cpp for CPU inference.
| Flag | Effect |
|------|--------|
| `-c 4096` | Reduces context window from the 32,768 default, freeing RAM for faster inference |
| `-t 8` | Uses all available CPU threads (default is only 4) — adjust to your machine's thread count |
| `Q4_K_M` quantization | Best balance of size, speed, and quality for CPU inference |
On Apple Silicon, switching to the [MLX engine](../architecture/engine.md) gives
significantly better performance than llama.cpp for most models.
---
## Common Errors
### `No such file or directory` when loading model
The path `path/to/model.gguf` in examples is a placeholder. Replace it with your actual
model path, e.g.:
```bash
llama-server -m ~/models/Qwen_Qwen3-4B-Q4_K_M.gguf
```
---
### `No module named 'openjarvis_rust'`
The Rust extension did not build correctly, or was built against the wrong Python version.
1. Confirm Python 3.12 is active: `uv run python --version`
2. Rebuild: `uv run maturin develop -m rust/crates/openjarvis-python/Cargo.toml`
If the version shows 3.14, go back to [Step 8](#step-8--pin-python-to-312).
---
### `PyO3 version error — Python 3.14 too new`
```
error: the configured Python interpreter version (3.14) is newer than
PyO3's maximum supported version (3.13)
```
PyO3 0.23.5 supports Python up to 3.13. Follow [Step 8](#step-8--pin-python-to-312) to
pin to 3.12, then delete `.venv`, recreate it, and restart your terminal before retrying.
---
### `Repository not found` when downloading model
bartowski's Qwen3 repos use the `Qwen_` prefix. Use:
```
bartowski/Qwen_Qwen3-4B-GGUF ✓
bartowski/Qwen3-4B-GGUF ✗
```
---
### `No inference engine available`
llama-server is not running. Start it in a separate terminal before running any `jarvis`
commands, and wait until you see `model loaded` in the output.
---
### Python version still shows 3.14 after recreating the venv
Close the terminal completely and reopen it. The old venv path is cached in the shell
environment and persists across commands until the session ends.
---
### `zsh: command not found: huggingface-cli`
When installed via `uv tool`, the CLI is invoked as `hf`, not `huggingface-cli`:
```bash
hf download ... # ✓
huggingface-cli download ... # ✗
```
---
## Next Steps
- [Quick Start](quickstart.md) — Run your first query and explore agents and tools
- [Configuration](configuration.md) — Customize engine hosts, model routing, memory, and more
- [Architecture](../architecture/overview.md) — Understand how OpenJarvis is structured
+23 -19
View File
@@ -9,20 +9,24 @@
var allRows = [];
var currentPage = 0;
// No-KV-cache formula: FLOPs = params_b * 1e9 * N * (N+1)
// Reference values only (GPT-5.3 constants) — recompute functions below
// are defined but not called; the leaderboard displays database values directly.
var DEFAULT_PARAMS_B = 137;
var ENERGY_WH_PER_FLOP = 0.4 / (1000 * 3e12);
// Outlier detection — hide entries with values that are physically
// implausible relative to their token count. Thresholds are ~1000x
// above legitimate per-token values to avoid false positives.
var MAX_ENERGY_WH_PER_TOKEN = 10; // legit ≈ 0.001 Wh/tok
var MAX_FLOPS_PER_TOKEN = 1e17; // legit ≈ 1e12 /tok
var MAX_DOLLAR_PER_TOKEN = 25.0 / 1e6; // hard ceiling: $25/1M output
function recomputeFlopsNoKvCache(totalTokens) {
var n = Number(totalTokens) || 0;
if (n <= 0) return 0;
return DEFAULT_PARAMS_B * 1e9 * n * (n + 1);
}
function recomputeEnergyNoKvCache(flops) {
return flops * ENERGY_WH_PER_FLOP;
function isOutlier(row) {
var tokens = Number(row.total_tokens) || 0;
if (tokens <= 0) return false;
var energy = Number(row.energy_wh_saved) || 0;
var flops = Number(row.flops_saved) || 0;
var dollars = Number(row.dollar_savings) || 0;
return (
energy / tokens > MAX_ENERGY_WH_PER_TOKEN ||
flops / tokens > MAX_FLOPS_PER_TOKEN ||
dollars / tokens > MAX_DOLLAR_PER_TOKEN
);
}
function escapeHtml(s) {
@@ -133,17 +137,17 @@
return;
}
allRows = rows;
allRows = rows.filter(function (r) { return !isOutlier(r); });
currentPage = 0;
var totalMembers = rows.length;
var totalMembers = allRows.length;
var totalDollars = 0;
var totalRequests = 0;
var totalTokens = 0;
for (var i = 0; i < rows.length; i++) {
totalDollars += Number(rows[i].dollar_savings || 0);
totalRequests += Number(rows[i].total_calls || 0);
totalTokens += Number(rows[i].total_tokens || 0);
for (var i = 0; i < allRows.length; i++) {
totalDollars += Number(allRows[i].dollar_savings || 0);
totalRequests += Number(allRows[i].total_calls || 0);
totalTokens += Number(allRows[i].total_tokens || 0);
}
var elMembers = document.getElementById("stat-members");
+3 -3
View File
@@ -16,7 +16,7 @@ See how the OpenJarvis community saves money, energy, and compute by running AI
<div class="lb-stat-value" id="stat-members">—</div>
</div>
<div class="lb-stat-card">
<div class="lb-stat-label">Total Saved</div>
<div class="lb-stat-label">Total Saved*</div>
<div class="lb-stat-value" id="stat-dollars">—</div>
</div>
<div class="lb-stat-card">
@@ -35,7 +35,7 @@ See how the OpenJarvis community saves money, energy, and compute by running AI
<tr>
<th style="width:50px">#</th>
<th>Name</th>
<th style="text-align:right">$ Saved</th>
<th style="text-align:right">$ Saved*</th>
<th style="text-align:right">Energy (Wh)</th>
<th style="text-align:right">FLOPs</th>
<th style="text-align:right">Requests</th>
@@ -55,5 +55,5 @@ See how the OpenJarvis community saves money, energy, and compute by running AI
<div id="leaderboard-pagination" class="lb-pagination"></div>
<p style="font-size:12px;opacity:0.6;margin-top:12px">
*Dollar savings estimates assume local open-source models (e.g. Qwen, Nemotron, Kimi) produce roughly the same number of tokens per request, on average, as closed-source cloud models.
*Dollar savings estimated vs. Claude Opus 4.6 API pricing ($5/1M input, $25/1M output tokens). Assumes local open-source models produce roughly the same number of tokens per request as cloud models.
</p>
+75
View File
@@ -0,0 +1,75 @@
# Agent QA Runbook
Manual testing scenarios for persistent agents in the CLI and desktop app.
## Environment Setup
| Prerequisite | Command / Check |
|---|---|
| Ollama running with model | `ollama list` shows `qwen3:8b` |
| OpenJarvis initialized | `uv run jarvis doctor` all green |
| Rust extension built | `uv run maturin develop -m rust/crates/openjarvis-python/Cargo.toml` |
| Desktop app running | `uv run jarvis serve` + `cd frontend && npm run dev` |
| Slack credentials | `SLACK_BOT_TOKEN`, `SLACK_APP_TOKEN` set, bot invited to test channel |
| Gmail credentials | OAuth credentials.json downloaded, token generated |
| Twitter credentials | All 5 env vars set (bearer + OAuth 1.0a) |
| Discord credentials | Bot token set, bot invited to test server |
| Telegram credentials | Bot token from @BotFather, test chat ID known |
| Email credentials | SMTP/IMAP host + credentials for test account |
## CLI Agent Scenarios
| # | Scenario | Steps | Expected Result | Pass |
|---|----------|-------|-----------------|------|
| 1 | Template launch | `jarvis agents launch`, pick research_monitor | Agent created, config printed with curated tools | [ ] |
| 2 | Manual run | `jarvis agents run <id>` | Output shows reasoning + tool calls, status -> idle | [ ] |
| 3 | Immediate ask | `jarvis agents ask <id> "summarize recent AI news"` | Synchronous response in terminal | [ ] |
| 4 | Queued instruct | `jarvis agents instruct <id> "focus on diffusion"`, then `jarvis agents run <id>` | Queued -> delivered, response in `jarvis agents messages <id>` | [ ] |
| 5 | Status check | `jarvis agents status` after 3+ runs | total_runs, total_cost, last_run_at populated | [ ] |
| 6 | Pause/resume | `jarvis agents pause <id>`, verify skipped, `jarvis agents resume <id>`, verify fires | Status toggles correctly | [ ] |
| 7 | Daemon scheduling | `jarvis agents daemon` with interval agent (60s) | 3+ ticks fire on schedule, memory accumulates | [ ] |
| 8 | Budget exhaustion | Set max_cost=0.001, run until exceeded | Status becomes budget_exceeded | [ ] |
| 9 | Error recovery | Kill Ollama mid-tick, then `jarvis agents recover <id>` | Error -> recover -> idle with checkpoint | [ ] |
| 10 | Channel binding | `jarvis agents bind <id> --slack #test`, run tick | Agent sends to Slack | [ ] |
| 11 | Multi-agent | Launch 3 agents, different intervals, run daemon | All fire independently | [ ] |
| 12 | Template tools | Create from each template, `jarvis agents info <id>` | All curated default tools listed | [ ] |
## Desktop App Scenarios
| # | Scenario | Steps | Expected Result | Pass |
|---|----------|-------|-----------------|------|
| 1 | Template wizard | New Agent -> pick each template -> complete wizard | Agent appears in grid with correct config | [ ] |
| 2 | Custom agent | New Agent -> Custom -> manual schedule, pick tools, set credentials | Tools + creds saved, agent created | [ ] |
| 3 | Run Now | Click Run Now on agent card | Status dot: green -> blue -> green, stats increment | [ ] |
| 4 | Immediate chat | Interact tab -> type message -> send (immediate mode) | Response appears in chat UI | [ ] |
| 5 | Queued chat | Interact tab -> send (queued mode) -> click Run Now | Message delivered on tick, response appears | [ ] |
| 6 | Task management | Tasks tab -> create task -> run agent | Task status updates, findings populated | [ ] |
| 7 | Memory inspection | Run 3+ ticks -> Memory tab | Summary memory reflects agent's accumulated knowledge | [ ] |
| 8 | Trace inspection | Run tick -> Logs tab | Trace steps visible with tool calls and results | [ ] |
| 9 | Learning | Enable trace-driven learning -> Learning tab -> trigger | Learning log entries appear | [ ] |
| 10 | Error + recovery | Stop Ollama -> run agent -> verify error badge -> click Recover | Error state shown, recovery resets to idle | [ ] |
## Channel-Specific QA Matrix
| Channel | Send Test | Receive Test | Thread/Reply Test | Agent Template | Pass |
|---------|-----------|-------------|-------------------|----------------|------|
| Slack | Post to #test-channel | Socket Mode incoming msg | Reply in thread (thread_ts) | inbox_triager | [ ] |
| Gmail | Send email to test recipient | Poll unread -> handler fires | Reply in thread (threadId) | inbox_triager | [ ] |
| Email (SMTP/IMAP) | Send via SMTP | IMAP poll UNSEEN | In-Reply-To header | inbox_triager | [ ] |
| iMessage (BlueBubbles) | Send to phone number | N/A (send-only) | N/A | research_monitor | [ ] |
| Twitter/X | Post tweet + send DM | Poll mentions | Reply (in_reply_to_tweet_id) | research_monitor | [ ] |
| Discord | Post to #test-channel | Gateway message event | N/A | code_reviewer | [ ] |
| Telegram | Send to test chat | Long-poll update | reply_to_message_id | research_monitor | [ ] |
| WhatsApp (Baileys) | Send to test number | Baileys incoming msg | N/A | inbox_triager | [ ] |
## Stress & Edge Cases
| # | Scenario | How to Test | Pass Criteria | Pass |
|---|----------|-------------|---------------|------|
| 1 | Message flood | Queue 50 messages via CLI, run tick | All 50 delivered, response generated | [ ] |
| 2 | Long-running daemon | Run daemon for 1 hour, 60s interval agent | No memory leak, no stall, ~60 ticks | [ ] |
| 3 | Rapid pause/resume | Script: pause -> resume -> pause -> resume during tick | Clean state, no corruption | [ ] |
| 4 | Credential revocation | Revoke Slack token mid-tick | Agent gets tool error, tick completes, status not corrupted | [ ] |
| 5 | Multi-agent load | 10 agents on daemon, mix of intervals | All fire on schedule, no interference | [ ] |
| 6 | Large response handling | Agent produces 10k+ char response | summary_memory truncated to 2000 chars, full response in messages | [ ] |
| 7 | Checkpoint integrity | Kill process mid-tick, restart, recover | Checkpoint restored, agent resumes cleanly | [ ] |
+23 -8
View File
@@ -626,7 +626,20 @@ These events enable the telemetry and trace systems to record detailed interacti
## Managed Agent Streaming
The Managed Agent API (`/v1/managed-agents/{id}/messages`) supports real-time SSE streaming. Send a message with `stream: true` to receive the agent's response as a Server-Sent Events stream instead of the default asynchronous queue mode.
The Managed Agent API (`/v1/managed-agents/{id}/messages`) supports **real LLM token streaming** via SSE. Send a message with `stream: true` to receive the model's response tokens as they are generated, rather than waiting for the full response.
### How It Works
The streaming endpoint calls `engine.stream_full()` directly, which yields `StreamChunk` objects containing content tokens, tool-call fragments, and finish reasons. This provides genuine token-by-token streaming from the LLM -- not a post-hoc word replay.
For multi-turn tool-calling agents, the streaming loop automatically:
1. Yields content tokens to the client as they arrive.
2. Accumulates tool-call fragments (OpenAI sends these incrementally).
3. Executes tools when `finish_reason="tool_calls"` is received.
4. Emits tool results as named SSE events (`event: tool_result`).
5. Feeds results back to the LLM for the next turn.
6. Repeats until the model produces a final text response or `max_turns` is reached.
### Streaming Messages
@@ -639,19 +652,21 @@ curl -N -X POST http://localhost:8000/v1/managed-agents/{id}/messages \
The response follows the OpenAI SSE format:
1. **Content chunks** -- `data: {"choices": [{"delta": {"content": "token"}}]}`
2. **Tool results** (if the agent used tools) -- `event: tool_results\ndata: {"results": [...]}`
3. **Final chunk** -- `data: {"choices": [{"delta": {}, "finish_reason": "stop"}]}`
4. **Done sentinel** -- `data: [DONE]`
2. **Tool calls** (if the model requests tool use) -- `event: tool_calls\ndata: {"calls": [{"tool_name": "...", "arguments": "..."}]}`
3. **Tool results** -- `event: tool_result\ndata: {"tool_name": "...", "output": "..."}`
4. **Final chunk** -- `data: {"choices": [{"delta": {}, "finish_reason": "stop"}]}`
5. **Done sentinel** -- `data: [DONE]`
When `stream: false` (the default), the endpoint behaves exactly as before -- the message is queued and the agent must be triggered separately via `/run`.
### Behavior Details
- The user message is always stored in the database before the agent runs.
- After streaming completes, the full agent response is persisted as an `agent_to_user` message.
- The agent is instantiated from the managed agent's stored `agent_type` and `config`.
- Conversation history from prior messages is automatically loaded as context.
- The user message is always stored in the database before streaming starts.
- After streaming completes, the full collected response is persisted as an `agent_to_user` message.
- Conversation history from prior messages is automatically loaded as LLM context.
- The engine's `stream_full()` method is used for real token streaming. Engines that do not override it fall back to the default implementation which wraps the plain `stream()` method.
- If the engine is not available on the server, a `503` error is returned.
- Tool execution during streaming uses the `ToolRegistry` to find and instantiate tools.
### Python Example
+158
View File
@@ -0,0 +1,158 @@
# External MCP Server Integration
OpenJarvis can extend agent capabilities by connecting to external [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) servers. This allows agents to use tools provided by services like Home Assistant, databases, custom APIs, or any MCP-compatible server -- without writing custom tool code.
## How It Works
When OpenJarvis starts, it reads the `[tools.mcp]` section in `config.toml`. For each configured server, it:
1. Opens a connection using the appropriate transport (Streamable HTTP or stdio).
2. Performs the MCP initialize handshake (protocol version negotiation and `initialized` notification).
3. Discovers available tools via `tools/list`.
4. Wraps each discovered tool as a standard `BaseTool` so agents can call them like any built-in tool.
If a server is unreachable or returns an error, OpenJarvis logs a warning and continues loading the remaining servers. One broken server does not prevent other tools from being available.
## Configuration
External MCP servers are configured in `config.toml` under `[tools.mcp]`:
```toml
[tools.mcp]
enabled = true
servers = '[{"name": "homeassistant", "url": "http://172.16.3.1:9583/private_abc123"}]'
```
The `servers` value is a **JSON-encoded string** containing an array of server objects. Each object defines one external MCP server.
!!! note
The value must be a JSON string (with single-quote TOML delimiters around it), not a native TOML array. This is because the configuration system passes it through as a single string field.
## Server Config Schema
Each server object supports the following fields:
| Field | Type | Required | Description |
|------------------|----------------|----------|----------------------------------------------------------|
| `name` | string | No | Human-readable name used in log messages. Defaults to `<unnamed>`. |
| `url` | string | No* | URL for Streamable HTTP transport. |
| `command` | string | No* | Command to launch a stdio-based MCP server. |
| `args` | list of strings| No | Arguments passed to the stdio command. |
| `include_tools` | list of strings| No | Whitelist of tool names to import. Only these tools are loaded. |
| `exclude_tools` | list of strings| No | Blacklist of tool names to skip. All other tools are loaded. |
*Either `url` or `command` must be provided. If neither is set, the server is skipped with a warning.
When both `include_tools` and `exclude_tools` are specified, the whitelist is applied first, then the blacklist filters the result.
## Examples
### Home Assistant via Streamable HTTP
Connect to the [ha-mcp](https://github.com/tevonsb/ha-mcp) Home Assistant add-on:
```toml
[tools.mcp]
enabled = true
servers = '[{"name": "homeassistant", "url": "http://172.16.3.1:9583/private_abc123"}]'
```
This discovers all HA tools (entity control, automations, history, etc.) and makes them available to agents.
### Stdio Server
Launch a local MCP server as a subprocess:
```toml
[tools.mcp]
enabled = true
servers = '[{"name": "myserver", "command": "python", "args": ["-m", "my_mcp_server"]}]'
```
OpenJarvis starts the process automatically, communicates via JSON-RPC over stdin/stdout, and terminates it on shutdown.
### Multiple Servers
```toml
[tools.mcp]
enabled = true
servers = '[{"name": "homeassistant", "url": "http://172.16.3.1:9583/private_abc123"}, {"name": "database", "command": "db-mcp-server", "args": ["--db", "postgres://localhost/mydb"]}]'
```
### Tool Filtering
When a server exposes many tools but you only need a few, use `include_tools` to whitelist:
```toml
[tools.mcp]
enabled = true
servers = '[{"name": "ha", "url": "http://172.16.3.1:9583/private_abc123", "include_tools": ["hassTurnOn", "hassTurnOff", "hassGetState"]}]'
```
To load everything except specific tools, use `exclude_tools`:
```toml
[tools.mcp]
enabled = true
servers = '[{"name": "ha", "url": "http://172.16.3.1:9583/private_abc123", "exclude_tools": ["hassCreateBackup", "hassDeleteBackup"]}]'
```
## Transport Types
### Streamable HTTP
Used when the `url` field is set. The transport sends JSON-RPC requests as HTTP POST to the given URL using `httpx`. It tracks the `Mcp-Session-Id` header across requests as required by the MCP Streamable HTTP specification.
**When to use:** Remote MCP servers, services running as HTTP endpoints (e.g., Home Assistant MCP add-on, cloud-hosted MCP servers).
**Connection parameters:**
- Connect timeout: 10 seconds
- Request timeout: 60 seconds
### Stdio
Used when the `command` field is set. OpenJarvis spawns the command as a subprocess and communicates via JSON-RPC lines on stdin/stdout.
**When to use:** Local MCP servers distributed as CLI tools, development/testing, servers that require filesystem access on the same machine.
!!! info "SSETransport alias"
`SSETransport` is provided as a backward-compatible alias for `StreamableHTTPTransport`. Both refer to the same implementation.
## Error Handling
OpenJarvis handles MCP server failures gracefully:
- **Server unreachable:** A warning is logged and the server is skipped. All other servers and built-in tools continue to load normally.
- **Timeout:** HTTP requests time out after 60 seconds. The server is skipped with a warning.
- **Invalid config:** If the `servers` JSON is malformed or a server entry has neither `url` nor `command`, a warning is logged and that entry is skipped.
- **Tool discovery failure:** If `tools/list` fails on a server, the error is caught and the server is skipped.
- **Runtime tool call failure:** If a tool call to an external MCP server fails at runtime, it returns a `ToolResult` with `success=False` and the error message.
No single server failure causes OpenJarvis to crash or prevents other tools from working.
## Troubleshooting
### Server not discovered
1. Check that `[tools.mcp]` has `enabled = true`.
2. Verify the `servers` JSON is valid. A common mistake is using TOML arrays instead of a JSON string.
3. Check the OpenJarvis logs for warnings like `Failed to discover external MCP tools`.
### Connection refused / timeout
1. Verify the server is running and reachable from the OpenJarvis host: `curl -v http://host:port/`.
2. Check firewall rules between the OpenJarvis container and the MCP server.
3. For Docker deployments, ensure both containers are on the same network or use host IPs.
### Tools not appearing
1. Run with debug logging to see which tools were discovered.
2. Check if `include_tools` or `exclude_tools` filters are too restrictive.
3. Verify the MCP server actually exposes tools via `tools/list` (some servers only expose resources or prompts).
### Stdio server crashes immediately
1. Test the command manually: `python -m my_mcp_server` should start and wait for input on stdin.
2. Check stderr output in the OpenJarvis logs for error messages from the subprocess.
3. Ensure all dependencies for the MCP server are installed in the same environment.
+1 -1
View File
@@ -204,7 +204,7 @@ All built-in tools are registered via `@ToolRegistry.register()` and are availab
| **Scheduler** | `pause_scheduled_task` | Pause an active scheduled task |
| **Scheduler** | `resume_scheduled_task` | Resume a paused scheduled task |
| **Scheduler** | `cancel_scheduled_task` | Cancel a scheduled task permanently |
| **Integration** | `mcp_adapter` | Bridge to external MCP tool servers |
| **Integration** | `mcp_adapter` | Bridge to external MCP tool servers (see [External MCP Servers](mcp-external-servers.md)) |
---