# Arsenal Tools
The **Arsenal** system (`crates/paladin-ports/src/output/arsenal_port.rs`) gives Paladins access
to external tools and services through the **Model Context Protocol** (MCP). Tools are called
*Armaments*; the registry that holds them is the *Arsenal*.
---
## Table of Contents
1. [Concepts](#concepts)
2. [Quick Start — STDIO Server](#quick-start--stdio-server)
3. [SSE Server Configuration](#sse-server-configuration)
4. [config.yml Reference](#configyml-reference)
5. [ArsenalPort Trait](#arsenalport-trait)
6. [ArsenalRegistry Trait](#arsenalregistry-trait)
7. [Attaching Arsenal to a Paladin](#attaching-arsenal-to-a-paladin)
8. [Custom Armaments (Direct Rust Tools)](#custom-armaments-direct-rust-tools)
9. [Handoff Tool](#handoff-tool)
10. [Error Handling](#error-handling)
11. [Best Practices](#best-practices)
---
## Concepts
| **Armament** | A single callable tool (name, description, JSON schema) |
| **ArmamentCall** | A runtime invocation (tool name + argument map) |
| **ArmamentResult** | Return value (`success: bool`, `output: Option<Value>`, `error: Option<String>`) |
| **ArsenalPort** | Trait for discovering and invoking armaments |
| **ArsenalRegistry** | Trait for managing the registry lifecycle (register, remove) |
| **MCPStdioAdapter** | Communicates with command-line MCP servers via stdin/stdout |
| **MCPSseAdapter** | Communicates with HTTP-based MCP servers via SSE |
---
## Quick Start — STDIO Server
STDIO servers are the most common MCP transport. The process is spawned and communicated with
via newline-delimited JSON on stdin/stdout.
### 1. Configure in `config.yml`
```yaml
arsenal:
mcp_servers:
- name: web_search
type: stdio
command: uvx
args: ["mcp-server-brave-search"]
env:
BRAVE_API_KEY: "${BRAVE_API_KEY}"
- name: filesystem
type: stdio
command: npx
args: ["-y", "@modelcontextprotocol/server-filesystem", "/workspace"]
```
### 2. Build a Paladin with the Arsenal
```rust,ignore
use paladin::application::services::paladin::paladin_builder::PaladinBuilder;
use paladin_ports::output::llm_port::LlmPort;
use paladin_ports::output::arsenal_port::ArsenalRegistry;
use std::sync::Arc;
// Arsenal registry is built from config.yml automatically when using
// PaladinBuilder::from_config() or can be constructed manually.
let paladin = PaladinBuilder::new(llm_port)
.system_prompt("You are a research assistant with web search access.")
.with_arsenal_registry(arsenal_registry)
.build()
.await?;
let result = paladin.execute("Find the latest Rust release notes").await?;
println!("{}", result.output);
```
The Paladin will automatically detect tool-call JSON in LLM responses, invoke the tool via
the Arsenal, and feed results back into the reasoning loop.
---
## SSE Server Configuration
HTTP/SSE servers expose a REST endpoint:
```yaml
arsenal:
mcp_servers:
- name: my_api_server
type: sse
endpoint: "http://localhost:8080/mcp"
timeout_seconds: 30
max_retries: 3
```
The `MCPSseAdapter` sends requests and reads responses over the SSE stream:
```rust,ignore
use paladin::infrastructure::adapters::arsenal::mcp_sse_adapter::MCPSseAdapter;
let mut adapter = MCPSseAdapter::new("http://localhost:8080/mcp");
adapter.connect().await?;
```
---
## config.yml Reference
```yaml
arsenal:
mcp_servers:
- name: <identifier> # Unique name used in logs and errors
type: stdio | sse # Transport type
# STDIO fields:
command: <executable> # e.g. python3, npx, uvx
args: [<arg>, ...] # Command-line arguments
env: # Optional environment variables
KEY: value
# SSE fields:
endpoint: <url> # Full URL of the SSE endpoint
timeout_seconds: 30 # Request timeout
max_retries: 3 # Retry attempts on failure
```
---
## ArsenalPort Trait
Defined in `crates/paladin-ports/src/output/arsenal_port.rs`:
```rust,ignore
#[async_trait]
pub trait ArsenalPort: Send + Sync {
/// List all available armaments from this MCP server
async fn list_armaments(&self) -> Vec<Armament>;
/// Invoke an armament with the given arguments
async fn invoke(&self, call: ArmamentCall) -> Result<ArmamentResult, ArsenalError>;
/// Validate call arguments against the armament's JSON schema
fn validate_call(&self, call: &ArmamentCall) -> Result<(), ArsenalError>;
}
```
Direct usage:
```rust,ignore
use paladin_core::platform::container::arsenal::ArmamentCall;
use serde_json::json;
use std::collections::HashMap;
let mut args = HashMap::new();
args.insert("query".to_string(), json!("Rust 2024 edition features"));
let call = ArmamentCall::new("web_search", args);
arsenal_port.validate_call(&call)?;
let result = arsenal_port.invoke(call).await?;
if result.success {
println!("{}", result.output.unwrap());
}
```
---
## ArsenalRegistry Trait
Defined alongside `ArsenalPort`:
```rust,ignore
#[async_trait]
pub trait ArsenalRegistry: Send + Sync {
/// Register a new armament in the registry
async fn register(&self, armament: Armament);
/// Remove an armament by name
async fn remove(&self, name: &str);
/// Get all registered armament descriptors
async fn list(&self) -> Vec<Armament>;
/// Look up a specific armament by name
async fn get(&self, name: &str) -> Option<Armament>;
}
```
---
## Attaching Arsenal to a Paladin
```rust,ignore
use paladin::application::services::paladin::paladin_builder::PaladinBuilder;
use paladin_ports::output::arsenal_port::ArsenalRegistry;
use std::sync::Arc;
let paladin = PaladinBuilder::new(llm_port)
.system_prompt(
"You are a coding assistant. Use the filesystem tool to read files when needed."
)
.with_arsenal_registry(Arc::new(my_registry))
.build()
.await?;
```
---
## Custom Armaments (Direct Rust Tools)
Implement `ArsenalPort` to expose any Rust function as a tool:
```rust,ignore
use async_trait::async_trait;
use paladin_core::platform::container::arsenal::{
Armament, ArmamentCall, ArmamentResult, ArsenalError,
};
use paladin_ports::output::arsenal_port::ArsenalPort;
pub struct CalculatorTool;
#[async_trait]
impl ArsenalPort for CalculatorTool {
async fn list_armaments(&self) -> Vec<Armament> {
vec![Armament {
name: "calculate".to_string(),
description: "Evaluate a mathematical expression".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"expression": { "type": "string" }
},
"required": ["expression"]
}),
}]
}
async fn invoke(&self, call: ArmamentCall) -> Result<ArmamentResult, ArsenalError> {
let expr = call.args["expression"].as_str().unwrap_or_default();
// ... evaluate ...
Ok(ArmamentResult { success: true, output: Some(serde_json::json!(42)), error: None })
}
fn validate_call(&self, call: &ArmamentCall) -> Result<(), ArsenalError> {
if call.args.contains_key("expression") {
Ok(())
} else {
Err(ArsenalError::InvalidArguments("expression is required".into()))
}
}
}
```
---
## Handoff Tool
The `handoff_tool` in `crates/paladin-core/src/platform/container/arsenal/handoff_tool.rs`
is a built-in Armament that allows a Paladin to delegate sub-tasks to specialist agents
at runtime. Register specialist agents on the builder:
```rust,ignore
let coordinator = PaladinBuilder::new(llm_port)
.system_prompt("You are a coordinator. Delegate to specialists when needed.")
.with_specialist(Arc::new(code_paladin))
.with_specialist(Arc::new(test_paladin))
.build()
.await?;
```
The LLM will emit a tool-call for `handoff` when it determines a specialist is more
appropriate. Delegation records appear in `PaladinResult.handoff_history`.
---
## Error Handling
`ArsenalError` variants (from `paladin_core::platform::container::arsenal`):
| `ToolNotFound(String)` | Armament name not in registry | Check `list_armaments()` |
| `InvalidArguments(String)` | Schema validation failed | Fix argument map |
| `Timeout` | Tool took too long | Increase `timeout_seconds` in config |
| `ProtocolError(String)` | Malformed MCP message | Check MCP server logs |
| `TransportError(String)` | Process/network failure | Verify server is running |
---
## Best Practices
- **Validate before invoking** — call `validate_call()` to catch argument errors early.
- **Set timeouts** — all MCP servers should have `timeout_seconds` to avoid blocking the
reasoning loop indefinitely.
- **Describe tools well** — the Armament `description` is what the LLM reads to decide
whether to call the tool; make it precise.
- **Namespace tool names** — use `server_name.tool_name` convention to avoid collisions
when registering multiple servers.
- **Test with mock** — implement a `MockArsenalPort` in tests to avoid spawning real subprocesses.