Ryvos can be extended with custom tools in three ways, from easiest to most powerful:
- Skills — Lua or Rhai scripts with a TOML manifest (easiest, no compilation)
- MCP Servers — External processes via the Model Context Protocol (any language)
- Rust Trait — Native Rust tools compiled into the binary (most performant)
Skills (Lua/Rhai Scripts)
Skills are the simplest way to add custom tools. A skill is a directory containing a manifest (skill.toml), a script (run.lua or run.rhai), and an input schema (schema.json).
Skill Directory Structure
~/.ryvos/skills/
└── weather/
├── skill.toml # Manifest: name, description, requirements
├── run.lua # Script that implements the tool
└── schema.json # JSON Schema for the tool's input
Step 1: Create the Manifest
# skill.toml
name = "weather"
description = "Get the current weather for a city"
version = "1.0.0"
command = "lua" # "lua" or "rhai"
[prerequisites]
required_binaries = [] # External binaries this skill needs
required_env = ["WEATHER_API_KEY"] # Environment variables required
required_os = [] # OS restrictions: ["linux", "macos", "windows"]Step 2: Define the Input Schema
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "City name (e.g., 'San Francisco')"
},
"units": {
"type": "string",
"enum": ["metric", "imperial"],
"default": "metric"
}
},
"required": ["city"]
}Step 3: Write the Script (Lua)
-- run.lua
-- Input is passed as JSON via stdin
local json = require("json")
local input = json.decode(io.read("*a"))
local city = input.city
local units = input.units or "metric"
local api_key = env("WEATHER_API_KEY")
local url = string.format(
"https://api.weatherapi.com/v1/current.json?key=%s&q=%s",
api_key, city
)
local response = http_get(url)
local data = json.decode(response)
local temp = data.current.temp_c
if units == "imperial" then
temp = data.current.temp_f
end
local result = string.format(
"%s: %s degrees %s, %s. Humidity: %s%%, Wind: %s km/h",
city, temp,
units == "metric" and "C" or "F",
data.current.condition.text,
data.current.humidity,
data.current.wind_kph
)
-- Output is returned as the tool result
print(result)Host Functions
Skills have access to these host functions:
| Function | Description |
|---|---|
read_file(path) | Read a file and return its contents |
http_get(url) | Make an HTTP GET request |
http_post(url, body) | Make an HTTP POST request with a body |
env(name) | Get an environment variable |
log(message) | Write to the Ryvos log |
Skill Execution
Skills run as subprocesses with JSON input on stdin, text output on stdout (becomes the ToolResult.content), 60-second timeout, and security tier T2 (medium) by default.
Skill Registry
Ryvos has a GitHub-hosted skill registry for sharing skills:
ryvos skill search weather # Search for skills
ryvos skill install weather # Install (verifies SHA-256 checksum)
ryvos skill list # List installed skills
ryvos skill remove weather # UninstallMCP Servers
The Model Context Protocol (MCP) lets you connect external tool servers written in any language. See MCP Integration for full details.
Quick example:
[mcp.servers.filesystem]
command = "npx"
args = ["-y", "@modelcontextprotocol/server-filesystem", "/home/user/projects"]MCP tools appear as mcp__filesystem__read_file, mcp__filesystem__write_file, etc.
Rust Trait (Native Tools)
For maximum performance, implement the Tool trait directly in Rust:
use ryvos_core::{Tool, ToolContext, ToolResult, RyvosError, SecurityTier};
use async_trait::async_trait;
use serde_json::{json, Value};
pub struct MyCustomTool;
#[async_trait]
impl Tool for MyCustomTool {
fn name(&self) -> &str { "my_custom_tool" }
fn description(&self) -> &str { "Does something custom and useful" }
fn input_schema(&self) -> Value {
json!({
"type": "object",
"properties": {
"input_field": {
"type": "string",
"description": "The input to process"
}
},
"required": ["input_field"]
})
}
fn tier(&self) -> SecurityTier { SecurityTier::T1 }
fn timeout_secs(&self) -> u64 { 30 }
async fn execute(
&self, input: Value, context: &ToolContext,
) -> Result<ToolResult, RyvosError> {
let field = input["input_field"].as_str()
.ok_or_else(|| RyvosError::InvalidInput("input_field required".into()))?;
Ok(ToolResult { content: format!("Processed: {}", field), is_error: false })
}
}Register in ryvos-tools/src/lib.rs:
registry.register(Arc::new(MyCustomTool));:::note Native Rust tools require recompiling Ryvos. For most use cases, Skills or MCP servers are more practical. :::
Comparison
| Feature | Skills | MCP | Rust Trait |
|---|---|---|---|
| Language | Lua, Rhai | Any | Rust |
| Setup | Drop-in directory | Config + running server | Compile |
| Performance | Subprocess (ms) | Subprocess/HTTP (ms) | Native (sub-ms) |
| Distribution | Skill registry (SHA-256) | npm, pip, etc. | Fork + compile |
| Default tier | T2 | T2 | Configurable |
Tool Naming Conventions
| Source | Pattern | Example |
|---|---|---|
| Built-in | tool_name | bash, read, memory_search |
| MCP | mcp__{server}__{tool} | mcp__filesystem__read_file |
| Skills | skill__{name} | skill__weather |
Next Steps
- Built-in Tools — All 86+ built-in tools
- MCP Overview — Model Context Protocol integration
- MCP Connecting — Stdio and SSE transport setup