DocsToolsCustom Tools & Skills

Ryvos can be extended with custom tools in three ways, from easiest to most powerful:

  1. Skills — Lua or Rhai scripts with a TOML manifest (easiest, no compilation)
  2. MCP Servers — External processes via the Model Context Protocol (any language)
  3. 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:

FunctionDescription
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          # Uninstall

MCP 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

FeatureSkillsMCPRust Trait
LanguageLua, RhaiAnyRust
SetupDrop-in directoryConfig + running serverCompile
PerformanceSubprocess (ms)Subprocess/HTTP (ms)Native (sub-ms)
DistributionSkill registry (SHA-256)npm, pip, etc.Fork + compile
Default tierT2T2Configurable

Tool Naming Conventions

SourcePatternExample
Built-intool_namebash, read, memory_search
MCPmcp__{server}__{tool}mcp__filesystem__read_file
Skillsskill__{name}skill__weather

Next Steps