Chaz supports external tools via the Model Context Protocol (MCP). MCP servers plug into the same registry the built-ins use and are subject to the same policy layer (risk, approval, grants, leak detection). Two transports are supported:
- stdio — chaz spawns a subprocess and speaks JSON-RPC over its stdin/stdout. Pick this for local tools (
npx @modelcontextprotocol/server-filesystem,uvx mcp-server-git, etc.). - Streamable HTTP — chaz POSTs requests to a URL and reads JSON or SSE responses. Pick this for remote/hosted servers.
Adding a server is config-only — no code changes, no rebuild.
Add MCP servers in the mcp_servers section of your config. Stdio and HTTP servers can coexist:
mcp_servers:
# Stdio transport — chaz spawns and supervises the subprocess.
- name: filesystem
command: npx
args: ["-y", "@modelcontextprotocol/server-filesystem", "/home/user"]
env:
SOME_VAR: "value"
default_policy:
risk: medium
approval: unless_auto_approved
timeout: 30
- name: github
command: npx
args: ["-y", "@modelcontextprotocol/server-github"]
env:
GITHUB_TOKEN: "${GITHUB_TOKEN}"
# Streamable HTTP transport — chaz POSTs to a remote server.
- name: remote-search
url: "https://mcp.example.com/v1"
default_policy:
risk: low
approval: never| Field | Required | Description |
|---|---|---|
name |
Yes | Namespace prefix for tools (e.g., filesystem → filesystem.read_file) |
command |
Stdio | Command to spawn the MCP server subprocess |
args |
No | Arguments for the command (stdio only) |
env |
No | Environment variables (supports ${VAR} references; stdio only) |
url |
HTTP | Endpoint URL for Streamable HTTP transport — when set, command is ignored |
default_policy |
No | Default ToolPolicy for every tool from this server |
startup_timeout_secs |
No | Seconds to allow for handshake + tool discovery before marking the server failed (default 30) |
Set exactly one of command (stdio) or url (HTTP) per server. You can also drop one MCP server config per file into mcp_server_dir (see Configuration); those entries are merged with the inline list at startup.
For each configured server, chaz:
- Spawns the MCP server subprocess
- Performs the MCP
initializehandshake — records which primitives (tools, resources, prompts) the server advertises - Calls
tools/listto discover available tools (only when the server claims thetoolscapability) - Registers each tool as
server_name__tool_name(e.g.,filesystem__read_file) - Adds capability wrapper tools for any other primitives the server claimed (see Resources and Prompts)
Failed servers are logged and skipped. Name collisions across servers are detected and duplicates are skipped with a warning.
Servers start in the background, concurrently with each other, and chaz does not wait for any of them. The peer is interactive from the first frame whether a server takes 40 ms or 20 seconds.
Tools become available as their server finishes. A turn's tool list is assembled when the turn starts, so a server that lands mid-conversation is advertised on the next turn — no restart, no reload. A server that lands mid-turn is callable but not yet advertised; the model will not know to ask for it until the turn after.
If the model calls a tool whose server is still starting — from a resumed conversation, say, where the name appears earlier in the transcript — it is told the server is still starting and that the call is worth retrying, rather than being told the tool does not exist.
startup_timeout_secs (default 30) bounds one server's handshake and discovery. A server that overruns it is marked failed and contributes no tools; it does not hold anything else up.
One-shot runs are the exception. chaz --print gets exactly one turn, so it waits for every server to settle before running it — deferring a load that single turn needs would produce an answer with no MCP tools rather than a faster one. This costs no more than blocking startup used to, since servers start concurrently. chaz cmd runs no turn at all and never waits.
Server state is visible in the TUI under Peer → MCP, where every configured server appears immediately as starting… and then resolves to running or failed. starting is not a failure — it means the connection is still in flight.
The MCP spec defines three primitives a server can expose: Tools, Resources, and Prompts. Chaz consumes all three. Whichever primitives a server advertises in its initialize response light up the corresponding wrapper tools:
| Server advertises | Wrapper tools added |
|---|---|
resources |
<server>__list_resources, <server>__read_resource(uri) |
prompts |
<server>__list_prompts, <server>__get_prompt(name, ...) |
The model uses these like any other tool: list_resources returns URIs + metadata, read_resource fetches one by URI. For prompts, list_prompts shows declared argument shapes (required args marked with *) and get_prompt renders the template into a single assistant-readable block.
If a server only advertises tools, none of these wrappers appear. There's no per-server config knob to disable them — they follow what the server claims.
read_resource concatenates every text content block the server returns. Binary blobs are summarized inline as [binary <mime>: ~N bytes] rather than dumped — chaz never spills decoded binary into the model's context. The same 100 KB truncation that bounds tool output bounds resource reads.
get_prompt returns the rendered template as a tool result (each message tagged [role] body). It does NOT replace the agent's system prompt — the model decides what to do with the content. Use this for parameterized prompt libraries the server exposes (summarize, explain-code, etc.).
If a stdio MCP server process crashes (detected via IO errors when calling a tool), chaz automatically restarts it:
- Exponential backoff: 1s, 2s, 4s, 8s, 16s between attempts
- Max attempts: 5 consecutive restarts before giving up
- Counter reset: The restart counter resets to zero after a successful tool call
- Re-initialization: After restart, the MCP handshake and tool discovery are repeated
This means transient subprocess crashes are handled transparently — the tool call that triggered the restart is retried after a successful restart. HTTP transport has no equivalent — chaz just surfaces the error to the caller.
Both transports honour the spec's notifications/tools/list_changed: when a server signals its tool set has changed, chaz lazily re-fetches tools/list before the next call. There's no need to restart chaz to pick up new tools.
Each MCP tool resolves to a ToolPolicy in this precedence order:
security.tool_policies.<namespaced_name>— per-tool override in chaz config; wins unconditionally.mcp_servers[*].default_policy— per-server pin in chaz config.- Annotations from
tools/list— the server self-reports its behavior via the MCP 2025-06annotationsfield. Chaz maps:destructiveHint: true→risk: high,approval: alwaysreadOnlyHint: true→risk: low,approval: never- (
destructivewins if both are set; misconfigured servers fail closed)
- chaz default —
risk: medium,approval: unless_auto_approved,timeout: 60.
A well-behaved MCP server can therefore ship without any chaz-side policy boilerplate; chaz trusts the server's self-declaration unless you pin a different value:
mcp_servers:
- name: filesystem
default_policy:
risk: low
approval: never
timeout: 120Per-tool override via security.tool_policies (uses the namespaced name):
security:
tool_policies:
filesystem__write_file:
approval: always
risk: medium
filesystem__read_file:
approval: never
risk: lowControl how MCP tool definitions are presented to the LLM using tool profiles. Glob patterns match namespaces:
tool_profiles:
compact:
default: full
tools:
"filesystem__*": brief
"github__*": summaryUse describe_tool for on-demand discovery when tools are in Brief or Summary mode:
{ "tool": "filesystem__read_file" }Agents can reference MCP tools by exact name or glob pattern:
agents:
- name: coder
allowed_tools:
- shell
- read_file
- write_file
- "filesystem__*" # All filesystem MCP toolsSee Agents for details on tool narrowing.
The shortest "agent reads a file through MCP" path:
-
Add the server to your config and (re)start chaz:
mcp_servers: - name: filesystem command: npx args: ["-y", "@modelcontextprotocol/server-filesystem", "/home/me/notes"]
-
Confirm registration. Chaz logs each server's handshake and discovered tool count as it finishes (look for
MCP '<name>'lines); the TUI's Peer → MCP page shows the same thing live. A failed handshake is logged and the server is skipped — chaz keeps running with whatever did register.In a session you can ask the agent itself:
What MCP tools are registered? Use describe_tool on any filesystem__* tool you find.describe_toolreturns the full schema and is what the LLM uses to discover details about tools hidden by tool profiles. -
Use them in a turn. The first call to a Medium-risk tool will trigger an approval prompt in the TUI:
List the files in /home/me/notes and read the first .md file.The agent calls
filesystem__list_directorythenfilesystem__read_file. Each call runs under chaz's policy layer — risk tier, approval, leak detection, timeout. -
If something fails: chaz logs the handshake failure and skips the server. Check the log for
MCP '<name>'lines, or open Peer → MCP and select the server for the error text. For stdio servers, fix the command/args and restart; for HTTP, verify the URL and reachability. A server stuck onstarting…is still within itsstartup_timeout_secs; it will resolve to failed when that elapses. -
To narrow which agents can see an MCP namespace, add a glob to the agent's
allowed_tools:agents: - name: notetaker allowed_tools: ["read_file", "filesystem__*"]
filesystem__*matches every tool that namespace exposes — present or future. The same glob form works in tool profiles for controlling how the LLM sees the tool definitions.
- No streaming: Tool results are returned as a single response, not streamed.
- Stdio sandboxing is process-level only: the MCP subprocess inherits chaz's environment. Pair sensitive servers with grants and per-agent
allowed_toolsrather than relying on the server itself for isolation.