agentleFS
radimsemradimsem/mithril/.claude/skills/tool-creator/SKILL.md

tool-creator

Skill16 starsChanged 7 months agoApache-2.0View on GitHub

Use when implementing new execution tools for the Mithril MCP server - covers file placement, trait implementation, argument design, validation rules, elicitation, availability checks, and router wiring

  • Installs packages

1 other project uses a version of this.

Adopted by

Most recently taken up by

  • Other versions1 project uses a version of this.

    Word for word · 0

    None found.

    Edited · 0

    None found.

    Wrote their own version · 1

    1. fritzprix/libr-agent★ 11same name

    How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.

  • In this repository2 other files it gives its agents.

The file

We split this skill into the rules it gives an agent

8 rules. About 0 are common, 2 are distinctive, and 6 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈2

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 6

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Before writing any code, verify the tool's CLI interface and sandbox compatibility.

    line 29
  2. Very specific:

    Run <cmd> --help (or <cmd> <subcommand> --help) to get the up-to-date flag reference.

    line 33
  3. Very specific:

    Run <cmd> --version to confirm the developer has a recent version installed:

    line 46
  4. Very specific:

    If the installed version is notably old (e.g.

    line 53
  5. Very specific:

    Always run <cmd> <subcommand> --help and build the Args struct from that output.

    line 67
  6. Very specific:

    Read the sandbox implementations in crates/sandbox/src/linux.rs (BubbleWrap) and crates/sandbox/src/darwin.rs (sandbox-exec) to reason …

    line 71
  7. Distinctive:

    Every tool file contains exactly three items:

    line 91
  8. Distinctive:

    Argument budget: 5-10 max: Only include args that AI coding agents use frequently or that are essential for regular use.

    line 107

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Tool Creator

Implementation guide for Mithril MCP server execution tools.

Overview

Each tool wraps a single command (POSIX, git, etc.) as a structured MCP tool. Tools are never shell strings - they build typed argument vectors validated before execution.

File Placement

Tool lives in crates/tools/src/<category>/<tool_name>.rs where category groups related commands:

CategoryExamples
system/Ls, Cp, Rm, Grep, Find, Sed, Awk
git/GitStatus, GitCommit, GitLog, GitDiff

PascalCase tool name, snake_case file name: GitStatus -> git/git_status.rs.

Register in crates/tools/src/<category>/mod.rs with pub mod <tool_name>;.

Pre-Implementation Verification

Before writing any code, verify the tool's CLI interface and sandbox compatibility. This section applies only to tools that wrap a CLI command — skip it for general-purpose tools like RunChain that implement execution logic rather than wrapping a binary.

1. CLI interface check

Run <cmd> --help (or <cmd> <subcommand> --help) to get the up-to-date flag reference. Use this output — not memory or documentation — to decide which arguments to expose in the Args struct.

# Examples
grep --help
git diff --help
cargo clippy --help

Cross-check the flag names, short forms, and value types against the help output before writing the parameters() JSON schema. Mismatches between the schema and the real CLI are the most common source of CmdError in tests.

2. Version check

Run <cmd> --version to confirm the developer has a recent version installed:

go version       # → go1.22.0 or newer
cargo --version  # → cargo 1.78.0 or newer

If the installed version is notably old (e.g. Go < 1.21, Cargo < 1.70), inform the developer to upgrade for reliable testing. If the command is not found at all, the tool should declare available() returning false (see ToolDef section below).

Trust --help over documentation or memory. Real CLI interfaces diverge from docs frequently. Common divergences found in this project:

Issue typeExample
Flag renamed/pluralizedJest --testPathPattern → --testPathPatterns
Semantics invertedMypy --show-error-codes → --hide-error-codes
Flag format changedGo mod -go version → -go=version (equals, not space)
Flag replaced by positionalCargoUpdate -p pkg → positional pkg
Flag doesn't existGlabPipelineView had --log/--job in docs but not in CLI
Allowed values expandedRuff output formats went from 4 to 13
State flag split into booleansGlab --state opened/closed → --closed/--merged/--all

Always run <cmd> <subcommand> --help and build the Args struct from that output.

3. Sandbox compatibility analysis

Read the sandbox implementations in crates/sandbox/src/linux.rs (BubbleWrap) and crates/sandbox/src/darwin.rs (sandbox-exec) to reason about whether the command can execute correctly under each sandbox level:

LevelWhat's availableKey constraints
DirFull rootfs read-only + cwd read-write + /dev + /proc + /tmpWrites outside cwd fail
ReadonlyFull rootfs read-only, no network even if requestedAll writes fail, network always denied
ContainerOnly /usr, /etc, merged-usr symlinks + cwd read-write + /dev + /proc + /tmpNo $HOME, no /var, no /opt unless bind-mounted

For each tool, answer:

  • Does the command need to write outside cwd? (e.g. cargo check writes to ~/.cargo/registry) → needs rw_binds in SandboxMeta
  • Does it need network access? (e.g. pip install, go get) → needs needs_network: true
  • Does it need read access to config outside /usr and /etc? (e.g. ~/.cargo/config.toml, ~/.npmrc) → needs ro_binds
  • Is the tool read-only? (e.g. cat, ls, git log) → should work at Readonly level
  • Is the tool mutating? (e.g. rm, sed, git commit) → Readonly must be rejected; set elicitable() -> true

This analysis informs both the SandboxMeta population in build_context() and the sandbox level used in tests.

Implementation Structure

Every tool file contains exactly three items:

1. Unit struct + Args struct

pub struct ToolName;

#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct ToolNameArgs {
    /// Required arg description.
    pub arg: String,
    /// Optional flag description (-f).
    pub flag: Option<bool>,
}

Argument budget: 5-10 max. Only include args that AI coding agents use frequently or that are essential for regular use. Skip obscure flags.

2. ToolDef implementation

impl ToolDef for ToolName {
    fn name(&self) -> Cow<'static, str> {
        Cow::Borrowed("ToolName")  // PascalCase, matches MCP tool name
    }

    fn description(&self) -> Cow<'static, str> {
        Cow::Borrowed("Short description for LLM context")
    }

    fn parameters(&self) -> Value {
        serde_json::json!({
            "type": "object",
            "properties": {
                "arg": {
                    "type": "string",
                    "description": "Arg description"
                },
                "flag": {
                    "type": "boolean",
                    "description": "Flag description (-f)"
                }
            },
            "required": ["arg"]
        })
    }

    // Override ONLY when needed:

    fn elicitable(&self) -> bool {
        true  // Tools that mutate project state (Rm, Mv, Sed, Patch, Touch, GitCommit...)
    }

    fn available(&self) -> bool {
        which::which("tool_binary").is_ok()  // Third-party or non-guaranteed commands
    }
}

Binary name fallback

Some tools have different binary names across Linux distros. Debian/Ubuntu renames tools to avoid conflicts:

Upstream nameDebian/Ubuntu nameReason
batbatcatAvoids collision with unrelated bat package
fdfdfindAvoids collision with fd network diagnostics tool
pippip3Python 3 distinction (prefer pip3)

Implement fallback with a helper function:

fn bat_binary() -> &'static str {
    if which::which("bat").is_ok() {
        "bat"
    } else {
        "batcat"
    }
}

impl ToolDef for Bat {
    fn available(&self) -> bool {
        which::which("bat").is_ok() || which::which("batcat").is_ok()
    }
    // ...
}

impl ToolExec for Bat {
    async fn build_context(&self, args: BatArgs, ctx: &mut ExecContext) {
        ctx.cmd.push(OsString::from(bat_binary()));
        // ...
    }
}

For pip, use the shared helper in crates/tools/src/python/mod.rs:

pub(crate) fn pip_bin() -> &'static str {
    if which::which("pip3").is_ok() { "pip3" } else { "pip" }
}

Always check the preferred name first, then fall back to the alternative.


**`parameters()` JSON schema must match `ToolNameArgs` field names and types exactly.** The schema is what the LLM sees; the struct is what gets deserialized.

### 3. `ToolExec` implementation

```rust
#[async_trait]
impl ToolExec for ToolName {
    type Args = ToolNameArgs;

    async fn build_context(&self, args: ToolNameArgs, ctx: &mut ExecContext) {
        // 1. Push binary name
        ctx.cmd.push(OsString::from("tool_binary"));

        // 2. Push flags from bool args
        if args.flag.unwrap_or(false) {
            ctx.cmd.push(OsString::from("-f"));
        }

        // 3. Push valued args
        if let Some(ref val) = args.optional_arg {
            ctx.cmd.push(OsString::from("--option"));
            ctx.cmd.push(OsString::from(val));
        }

        // 4. Push positional args last
        ctx.cmd.push(OsString::from(&args.arg));

        // 5. Populate sandbox metadata (when the tool needs special access)
        //    Set needs_network for commands that fetch from registries.
        //    Add ro_binds / rw_binds for config and cache dirs outside cwd.
        if let Some(home) = std::env::var_os("HOME").map(PathBuf::from) {
            ctx.sandbox.meta.ro_binds.push(home.join(".config/tool"));
            ctx.sandbox.meta.rw_binds.push(home.join(".cache/tool"));
        }
        ctx.sandbox.meta.needs_network = true; // only if the command fetches

        // 6. Register validation rules (CRITICAL - see below)
        ctx.tool.validation_rules.insert(
            "arg",
            ValidationRule::PathConfined {
                pattern: StringPattern::NON_EMPTY | StringPattern::NO_PATH_TRAVERSAL,
                melody: Some(patterns::MELODY_UNIX_PATH),
                allow_git: false,
            },
        );
    }
}

Sandbox Metadata

Tools that need resources beyond what the sandbox level provides must populate ctx.sandbox.meta in build_context(). The sandbox layer reads this metadata to add bind-mounts (Linux/bwrap) or SBPL rules (macOS/sandbox-exec).

FieldWhen to setExample tools
needs_network: trueCommand fetches from registries or remote hostsPipInstall, GoGet, NpmInstall, CargoFetch
ro_bindsCommand reads config/toolchain outside /usr and /etcCargo (.cargo/config.toml, .rustup), Go (.config/mise)
rw_bindsCommand writes to cache dirs outside cwdCargo (.cargo/registry), Go (~/go, .cache/go-build), pip (.cache/pip)

Rules:

  • rw_binds are only honoured under Dir and Container levels — Readonly ignores them.
  • rw_binds outside $HOME are silently rejected by both sandbox backends.
  • Read-only tools (e.g. cat, ls, git log) should leave SandboxMeta at its default.
  • Guard bind paths with if let Some(home) = std::env::var_os("HOME") — don't assume $HOME exists.
  • Include macOS-specific cache paths alongside Linux ones. macOS uses ~/Library/Caches/<tool> instead of ~/.cache/<tool>. Add both in the same helper.

Reference the ecosystem-specific meta helpers in crates/sandbox/src/linux.rs tests (pip_meta(), npm_meta(), cargo_meta(), go_meta()) for canonical bind-mount sets.

Shared sandbox meta helpers

Domain mod.rs files provide reusable meta helpers. Use these instead of inlining bind-mount logic in each tool.

HelperLocationWhat it sets
apply_git_meta(meta)crates/tools/src/git/mod.rs~/.gitconfig ro_bind
apply_git_network_meta(meta)crates/tools/src/git/mod.rsgit meta + needs_network
apply_docker_meta(meta)crates/tools/src/docker/mod.rs~/.docker ro_bind, /var/run/docker.sock ro_bind, needs_network
apply_pip_meta(meta, needs_network)crates/tools/src/python/mod.rs~/.config/pip ro, ~/.cache/pip + ~/Library/Caches/pip + ~/.local rw
apply_uv_meta(meta, needs_network)crates/tools/src/python/mod.rs~/.config/uv ro, ~/.cache/uv + ~/Library/Caches/uv + ~/.local rw

When adding a new tool to an existing domain, call the domain's meta helper:

// In git tool's build_context():
super::apply_git_meta(&mut ctx.sandbox.meta);

// In docker tool's build_context():
super::apply_docker_meta(&mut ctx.sandbox.meta);

// In pip tool's build_context():
super::apply_pip_meta(&mut ctx.sandbox.meta, true /* needs_network */);

When creating a new domain that needs shared sandbox config, add a helper in the domain's mod.rs following this pattern:

pub(crate) fn apply_example_meta(meta: &mut SandboxMeta) {
    if let Some(home) = std::env::var_os("HOME").map(PathBuf::from) {
        meta.ro_binds.push(home.join(".config/example"));
        meta.rw_binds.push(home.join(".cache/example"));
        // macOS cache location
        meta.rw_binds.push(home.join("Library/Caches/example"));
    }
}

Validation Rules - The Critical Part

Every non-bool argument MUST have a validation rule. Only register rules for args present in ctx.args (optional args guard with if ctx.args.contains_key("field")).

Rule selection guide

Arg typeRuleKey flags
File/dir pathPathConfinedNO_PATH_TRAVERSAL, NO_GIT_COMPONENT for mutating tools, allow_git: true only for git tools
Command nameString with NO_SPACESmelody: Some(patterns::MELODY_CMD_NAME), exempt_injection_guard: false
Free-text (commit msg, pattern)String with NON_EMPTYexempt_injection_guard: true - allows shell metacharacters the arg legitimately needs
Integer (line count, max)Integer(IntegerPattern::POSITIVE)or NON_NEGATIVE if zero is valid
Path listStrVectorSame StringRule as single path
Constrained stringAllowList(&["opt1", "opt2"])For args with fixed valid values

Injection guard exemption

exempt_injection_guard: false (default, strict) blocks shell metacharacters ($, `, |, ;, etc.).

Set exempt_injection_guard: true ONLY when the argument semantically requires those characters:

  • Grep/sed patterns ($, *, ., ^)
  • Commit messages (free-form text)
  • Awk programs

When in doubt, keep exempt_injection_guard: false.

Melody patterns

  • patterns::MELODY_UNIX_PATH - for file/directory path arguments
  • patterns::MELODY_CMD_NAME - for command/binary name arguments

Environment Variable Security

The validator blocks dangerous environment variables via ENV_BLOCKLIST in crates/validator/src/constants.rs. Key rules:

  • Both LD_* and DYLD_* are blocked unconditionally — never use #[cfg(target_os)] guards on security-relevant blocklists. A cross-compiled binary must still block the other platform's dangerous variables.
  • The envs global arg allows tools to inject environment variables, but the validator rejects any that match the blocklist before execution.
  • When a tool needs cache/config redirection (e.g. GOPATH, PIP_CACHE_DIR), these are safe to inject via envs because they don't appear in the blocklist.

Path Validation for Non-Existing Files

The validator's path confinement uses resolve_path() which handles files that don't exist yet:

fn resolve_path(path: &OsStr) -> io::Result<PathBuf> {
    match fs::canonicalize(path) {
        Ok(canon) => Ok(canon),
        Err(_) => {
            let parent = p.parent().unwrap_or(p);
            let name = p.file_name().unwrap_or_default();
            Ok(fs::canonicalize(parent)?.join(name))
        }
    }
}

This means path arguments for tools that create files (Touch, Tee, redirected output) will pass validation as long as the parent directory exists and is within the confined path. No special handling needed in build_context().

Router Wiring

After the tool implementation is reviewed, wire it in crates/server/src/router.rs:

1. Add import

use tools::system::tool_name::{ToolName, ToolNameArgs};
// or for a new category:
use tools::git::git_status::{GitStatus, GitStatusArgs};

2. Add endpoint method

#[tool(name = "ToolName", description = "Description matching ToolDef")]
async fn tool_name(
    &self,
    Parameters(tool_args): Parameters<ToolArgs<ToolNameArgs>>,
    req_ctx: RequestContext<RoleServer>,
) -> Result<CallToolResult, McpError> {
    self.exec_tool(&ToolName, tool_args, req_ctx).await
}

The #[tool] macro name must match ToolDef::name(). Description should match or closely mirror ToolDef::description().

Writing Tests

Every tool must have at least two tests in crates/server/tests/tools/<category>.rs. Tests exercise the tool through the full MCP pipeline (validation → proxy → sandbox → runner) using TestClient.

Test infrastructure

use serde_json::json;

use crate::{
    harness::{TempEnv, TextExtractor},
    tools::client_with_root,
};
  • TempEnv — creates an isolated temporary directory under /var/tmp/mithril_tests (avoids collision with bwrap's --tmpfs /tmp). Chain .with_file(path, content).await to scaffold files.
  • client_with_root(&cwd) — creates a TestClient with the given path as the project root.
  • TextExtractor::from_tool_result(&result) — extracts text content from the tool result for assertions.

Availability guard

If the tool wraps a binary that may not be installed, add a guard function and early-return:

fn tool_available() -> bool {
    std::process::Command::new("tool_binary")
        .arg("--version")
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .status()
        .map(|s| s.success())
        .unwrap_or(false)
}

#[tokio::test]
async fn tool_does_something() {
    if !tool_available() {
        return;
    }
    // ...
}

Sandbox level in tests

The sandbox field in GlobalArgs controls the isolation level per-call. Pass it in the JSON args:

// Read-only tool → test at Readonly level
client.call_tool("Cat", json!({
    "path": path.to_str().unwrap(),
    "sandbox": "readonly",
}), Some(&cwd)).await

// Mutating tool → test at Dir level (default)
client.call_tool("Rm", json!({
    "path": path.to_str().unwrap(),
}), Some(&cwd)).await

// Tool that should work in full isolation → test at Container level
client.call_tool("GoBuild", go_args(&cwd, json!({
    "sandbox": "container",
})), Some(&cwd)).await

Sandbox level selection for tests:

Tool typeDefault test sandboxRationale
Read-only (cat, ls, git log)"readonly"Proves the tool works without write access
Mutating (rm, sed, git commit)"dir" (default)Needs cwd write access
Ecosystem build tools"container" if feasibleProves the tool works with minimal system access + sandbox meta binds

Environment variables for ecosystem tools

Some tools need environment variables pointing caches inside cwd so they work under sandbox. Follow the Go test pattern:

fn tool_args(cwd: &std::path::Path, extra: Value) -> Value {
    let cache = cwd.join(".tool_cache");

    let mut map = match extra {
        Value::Object(m) => m,
        _ => serde_json::Map::new(),
    };

    map.insert(
        String::from("envs"),
        json!({ "TOOL_CACHE": cache.to_string_lossy() }),
    );

    Value::Object(map)
}

What to test

Each tool needs at least two tests. Prefer a mix of:

  1. Happy-path execution — call the tool with commonly-used arguments that AI coding agents would use in production. Assert !result.is_error.unwrap_or(false) and check the output contains expected content.
  2. Validation rejection — pass an invalid argument (empty string, zero integer, invalid enum value) and assert result.is_err().

Additional tests to consider:

  • Different argument combinations that agents use frequently
  • Ecosystem detection (tool fails gracefully without the expected project file, e.g. go.mod, Cargo.toml)

Auth-gated tools

Tools that require authentication (like gh, glab) cannot run happy-path tests in CI. For these, test only validation rejections — invalid arguments should be caught by the validation layer before any CLI execution or auth check occurs.

Diagnosing CmdError

When a test returns CmdError(...):

  1. Wrong CLI flags — the most common cause. Re-check <cmd> --help output against the args struct. A flag may have been renamed, removed, or may require a different value format.
  2. Sandbox resource denial — the command tried to access a path not available at the test sandbox level. Check whether the tool needs additional ro_binds / rw_binds in its SandboxMeta, or whether environment variables should redirect caches inside cwd.
  3. Missing environment variables — some tools need HOME, PATH, or tool-specific vars. Use the envs global arg or isolated_env: false (default) to inherit the host environment.
  4. Missing project scaffolding — the TempEnv may be missing required files (e.g. go.mod, Cargo.toml, package.json). Add them via .with_file().

Test file registration

Add the test module in crates/server/tests/tools/mod.rs:

pub mod <category>;  // e.g. pub mod docker;

If the tool belongs to an existing category (e.g. a new git tool → git.rs), add tests to the existing file under a new section header comment.

Checklist

Pre-implementation (CLI tools only)

  • Ran <cmd> <subcommand> --help to verify up-to-date CLI flags (trust --help, not docs/memory)
  • Ran <cmd> --version to confirm a recent version is installed
  • Analyzed sandbox levels (Dir/Readonly/Container) for resource compatibility
  • Checked if binary has alternative names across distros (bat/batcat, fd/fdfind, pip3/pip)

Implementation

  • File at crates/tools/src/<category>/<tool_name>.rs
  • Registered in <category>/mod.rs
  • Args struct with 5-10 max fields, doc comments on each
  • ToolDef: name (PascalCase), description, parameters JSON schema matches args struct
  • elicitable() -> true if tool mutates project state
  • available() uses which::which() if command isn't guaranteed on PATH; checks all binary name variants with ||
  • Binary name fallback helper if distro-specific names exist
  • build_context(): cmd vector built in correct order (binary, flags, options, positionals)
  • ctx.sandbox.meta populated via domain meta helper (e.g. apply_git_meta, apply_pip_meta) or inline if new domain
  • macOS cache paths included alongside Linux ones (~/Library/Caches/<tool> + ~/.cache/<tool>)
  • Every non-bool arg has a validation rule with appropriate strictness
  • exempt_injection_guard is true only where semantically required
  • Path args for mutating tools include NO_GIT_COMPONENT
  • Router endpoint added in crates/server/src/router.rs
  • #[tool] name matches ToolDef::name()

Tests

  • At least 2 tests in crates/server/tests/tools/<category>.rs
  • Tests use TempEnv for isolated temp environment with required project files
  • Read-only tools tested with "sandbox": "readonly" in args
  • Container-compatible tools tested with "sandbox": "container" where feasible
  • Ecosystem tools redirect caches inside cwd via envs to avoid sandbox denials
  • Auth-gated tools test only validation rejections (no happy-path execution)
  • Docker tools use docker info (not --version) in availability guard
  • Sandbox test names include ecosystem name with underscores for CI filtering (e.g. _pip_, _cargo_)
  • Test module registered in crates/server/tests/tools/mod.rs (if new category)

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.