* fix(mcp): header safety validation and Authorization conflict bug from #704 * fix(mcp): enforce RFC 9110 header validation on all config load paths Replace hand-written CRLF checks with reqwest::header::HeaderName::from_bytes() and HeaderValue::from_str(), catching spaces, colons, null bytes, and all non-token characters that the previous validation missed. Add validation to load_mcp_servers_from() and load_mcp_servers_from_db() so corrupted configs from disk or DB are rejected at load time instead of silently flowing through to McpClient. Improve app.rs error handling to distinguish "no config" from "corrupted config" (including malformed JSON). Also fix build_request_headers() to check self.custom_headers directly instead of indirectly via server_config, and clarify the wire test comment about HeaderMap::insert replacement semantics. * fix ci issue
Tool System
Adding a New Tool
Built-in Tools (Rust)
- Create
src/tools/builtin/my_tool.rs - Implement the
Tooltrait - Add
mod my_tool;andpub useinsrc/tools/builtin/mod.rs - Register in
ToolRegistry::register_builtin_tools()inregistry.rs - Add tests
WASM Tools (Recommended)
WASM tools are the preferred way to add new capabilities. They run in a sandboxed environment with explicit capabilities.
- Create a new crate in
tools-src/<name>/ - Implement the WIT interface (
wit/tool.wit) - Create
<name>.capabilities.jsondeclaring required permissions - Build with
cargo build --target wasm32-wasip2 --release - Install with
ironclaw tool install path/to/tool.wasm
See tools-src/ for examples.
Tool Architecture Principles
CRITICAL: Keep tool-specific logic out of the main agent codebase.
The main agent provides generic infrastructure; tools are self-contained units that declare their requirements through capabilities files.
What Goes in Tools (capabilities.json)
- API endpoints the tool needs (HTTP allowlist)
- Credentials required (secret names, injection locations)
- Rate limits and timeouts
- Auth setup instructions (see below)
- Workspace paths the tool can read
What Does NOT Go in Main Agent
- Service-specific auth flows (OAuth for Notion, Slack, etc.)
- Service-specific CLI commands (
auth notion,auth slack) - Service-specific configuration handling
- Hardcoded API URLs or token formats
Tool Authentication
Tools declare their auth requirements in <tool>.capabilities.json under the auth section. Two methods are supported:
OAuth (Browser-based login)
For services that support OAuth, users just click through browser login:
{
"auth": {
"secret_name": "notion_api_token",
"display_name": "Notion",
"oauth": {
"authorization_url": "https://api.notion.com/v1/oauth/authorize",
"token_url": "https://api.notion.com/v1/oauth/token",
"client_id_env": "NOTION_OAUTH_CLIENT_ID",
"client_secret_env": "NOTION_OAUTH_CLIENT_SECRET",
"scopes": [],
"use_pkce": false,
"extra_params": { "owner": "user" }
},
"env_var": "NOTION_TOKEN"
}
}
To enable OAuth for a tool:
- Register a public OAuth app with the service (e.g., notion.so/my-integrations)
- Configure redirect URIs:
http://localhost:9876/callbackthroughhttp://localhost:9886/callback - Set environment variables for client_id and client_secret
Manual Token Entry (Fallback)
For services without OAuth or when OAuth isn't configured:
{
"auth": {
"secret_name": "openai_api_key",
"display_name": "OpenAI",
"instructions": "Get your API key from platform.openai.com/api-keys",
"setup_url": "https://platform.openai.com/api-keys",
"token_hint": "Starts with 'sk-'",
"env_var": "OPENAI_API_KEY"
}
}
Auth Flow Priority
When running ironclaw tool auth <tool>:
- Check
env_var- if set in environment, use it directly - Check
oauth- if configured, open browser for OAuth flow - Fall back to
instructions+ manual token entry
The agent reads auth config from the tool's capabilities file and provides the appropriate flow. No service-specific code in the main agent.
WASM Tools vs MCP Servers: When to Use Which
Both are first-class in the extension system (ironclaw tool install handles both), but they have different strengths.
WASM Tools (IronClaw native)
- Sandboxed: fuel metering, memory limits, no access except what's allowlisted
- Credentials injected by host runtime, tool code never sees the actual token
- Output scanned for secret leakage before returning to the LLM
- Auth (OAuth/manual) declared in
capabilities.json, agent handles the flow - Single binary, no process management, works offline
- Cost: must build yourself in Rust, no ecosystem, synchronous only
MCP Servers (Model Context Protocol)
- Growing ecosystem of pre-built servers (GitHub, Notion, Postgres, etc.)
- Any language (TypeScript/Python most common)
- Can do websockets, streaming, background polling
- Cost: external process with full system access (no sandbox), manages own credentials, IronClaw can't prevent leaks
Decision guide:
| Scenario | Use |
|---|---|
| Good MCP server already exists | MCP |
| Handles sensitive credentials (email send, banking) | WASM |
| Quick prototype or one-off integration | MCP |
| Core capability you'll maintain long-term | WASM |
| Needs background connections (websockets, polling) | MCP |
| Multiple tools share one OAuth token (e.g., Google suite) | WASM |
The LLM-facing interface is identical for both (tool name, schema, execute), so swapping between them is transparent to the agent.