use crate::{ToolRateLimit, ToolSchema};
use serde_json::json;
pub fn shell() -> ToolSchema {
ToolSchema {
name: "shell".to_string(),
description: "Execute a shell command and return stdout/stderr.".to_string(),
parameters: json!({
"type": "object",
"properties": {
"command": {
"type": "string",
"description": "The shell command to execute"
},
"cwd": {
"type": "string",
"description": "Working directory (optional)"
},
"timeout_ms": {
"type": "integer",
"description": "Timeout in milliseconds (optional)"
}
},
"required": ["command"]
}),
returns: Some(json!({
"type": "object",
"properties": {
"stdout": { "type": "string" },
"stderr": { "type": "string" },
"exit_code": { "type": "integer" }
}
})),
idempotent: false,
cache_ttl_secs: None,
rate_limit: None,
}
}
pub fn read_file() -> ToolSchema {
ToolSchema {
name: "read_file".to_string(),
description: "Read a UTF-8 text file and return its contents. The returned \
`content` is LINE-NUMBERED in `cat -n` style: every line is \
prefixed with its 1-based line number, right-aligned in 6 columns, \
then a tab (e.g. ` 1\\tfn main() {`). Those prefixes are a \
display aid so you can cite exact line numbers (they line up with \
grep_files' `line`) — they are NOT part of the file. NEVER copy a \
prefix into edit_file's `old_text`/`new_text` or write_file's \
`content`; use only the raw text after the tab. Use `offset` \
(0-based line) and `limit` to page through a large file; numbering \
then starts at `offset + 1`, while `size_bytes` and `total_lines` \
always describe the FULL file. Reading a file also lets you edit it \
afterward: the runtime requires you to read an existing file before \
editing or overwriting it. A paged read can license one unique \
targeted edit, but use an unpaged read before replace_all, appending \
to, or overwriting an existing file."
.to_string(),
parameters: json!({
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Absolute or relative file path"
},
"offset": {
"type": "integer",
"description": "0-based starting line offset (optional). Line numbering in the output starts at offset + 1."
},
"limit": {
"type": "integer",
"description": "Maximum number of lines to return (optional)"
}
},
"required": ["path"]
}),
returns: Some(json!({
"type": "object",
"properties": {
"content": { "type": "string", "description": "File text, line-numbered `cat -n` style (strip the `%6d\\t` prefix before reusing any line)" },
"size_bytes": { "type": "integer", "description": "Byte length of the FULL file" },
"total_lines": { "type": "integer", "description": "Line count of the FULL file" }
}
})),
idempotent: true,
cache_ttl_secs: None,
rate_limit: None,
}
}
pub fn edit_file() -> ToolSchema {
ToolSchema {
name: "edit_file".to_string(),
description: "Make a targeted edit to an existing file by replacing `old_text` \
with `new_text`. Prefer this over write_file for changing part of \
a file — it never risks clobbering the rest. `old_text` and \
`new_text` must be the EXACT raw file text: do NOT include the \
line-number prefixes that read_file displays (the `%6d\\t` before \
each line are display-only — copy only the text after the tab). By \
default `old_text` must match EXACTLY ONE place in the file; if it \
matches several, add surrounding lines to make it unique, or set \
`replace_all: true` to replace every occurrence. You must read the \
file (read_file) earlier in this session before editing it, and \
re-read it if it changed on disk since — the runtime rejects an \
edit to a file you have not read (or that is stale). A paged read is \
sufficient only for one unique targeted replacement; replace_all \
requires a fresh unpaged read."
.to_string(),
parameters: json!({
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Absolute or relative file path"
},
"old_text": {
"type": "string",
"description": "Existing text to replace, verbatim (no read_file line-number prefixes). Must match uniquely unless `replace_all` is true."
},
"new_text": {
"type": "string",
"description": "Replacement text (no read_file line-number prefixes)"
},
"replace_all": {
"type": "boolean",
"description": "Replace every occurrence of `old_text` instead of requiring a unique match (default: false)"
}
},
"required": ["path", "old_text", "new_text"]
}),
returns: Some(json!({
"type": "object",
"properties": {
"edited": { "type": "string" },
"diff_summary": { "type": "string" },
"replacements": { "type": "integer", "description": "Number of occurrences replaced" }
}
})),
idempotent: false,
cache_ttl_secs: None,
rate_limit: None,
}
}
pub fn write_file() -> ToolSchema {
ToolSchema {
name: "write_file".to_string(),
description: "Write `content` to a file, creating it if it does not exist. Use \
this to CREATE a new file or fully replace one; to change part of \
an existing file, prefer edit_file (a whole-file overwrite is \
easy to get wrong). Overwriting an existing file requires you to \
have read its FULL current content with an unpaged read_file earlier \
in this session — the runtime rejects a blind overwrite of a file \
you have not read; creating a NEW file needs no prior read. Appending \
to an existing file has the same full-read requirement. Set \
`append: true` to append instead of overwrite. `content` is written \
verbatim — do NOT include the \
line-number prefixes read_file displays."
.to_string(),
parameters: json!({
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Absolute or relative file path"
},
"content": {
"type": "string",
"description": "Content to write, verbatim (no read_file line-number prefixes)"
},
"append": {
"type": "boolean",
"description": "Append instead of overwrite (default: false)"
}
},
"required": ["path", "content"]
}),
returns: Some(json!({
"type": "object",
"properties": {
"bytes_written": { "type": "integer" }
}
})),
idempotent: false,
cache_ttl_secs: None,
rate_limit: None,
}
}
pub fn find_files() -> ToolSchema {
ToolSchema {
name: "find_files".to_string(),
description: "Find files by name or glob pattern within a directory tree. \
A bare pattern (no '/') matches file names at any depth \
(e.g. `*.rs` finds every Rust file); a pattern with '/' \
matches the path relative to the search root, and `**` \
spans directories (e.g. `src/**/*.rs`)."
.to_string(),
parameters: json!({
"type": "object",
"properties": {
"pattern": {
"type": "string",
"description": "File name or glob pattern. Supports `*` (within a path segment), `**` (across directories), and `?`. A pattern containing `/` is matched against the path relative to the search root; a bare pattern is matched against the file name at any depth."
},
"path": {
"type": "string",
"description": "Root search path (default: .)"
},
"max_results": {
"type": "integer",
"description": "Maximum number of matching files to return (default: 1000)"
}
},
"required": ["pattern"]
}),
returns: Some(json!({
"type": "object",
"properties": {
"files": {
"type": "array",
"items": { "type": "string" }
},
"count": { "type": "integer" },
"truncated": { "type": "boolean" }
}
})),
idempotent: true,
cache_ttl_secs: None,
rate_limit: None,
}
}
pub fn grep_files() -> ToolSchema {
ToolSchema {
name: "grep_files".to_string(),
description: "Search file contents recursively with a regex, returning each \
match as `{path, line, text}`. `line` is 1-based and matches the \
line numbers read_file shows, so grep to locate code and then \
read_file/edit_file that exact spot. Only text source files are \
scanned (binary and oversized files are skipped) and \
dotfiles/`node_modules`/`__pycache__`/`target` are ignored. Use \
`max_results` to bound output (default 50). Use find_files instead \
to locate files by NAME rather than content."
.to_string(),
parameters: json!({
"type": "object",
"properties": {
"pattern": {
"type": "string",
"description": "Regex pattern to search for"
},
"path": {
"type": "string",
"description": "Root search path (default: .)"
},
"max_results": {
"type": "integer",
"description": "Maximum number of matching lines to return (default: 50)"
}
},
"required": ["pattern"]
}),
returns: Some(json!({
"type": "object",
"properties": {
"matches": {
"type": "array",
"items": {
"type": "object",
"properties": {
"path": { "type": "string" },
"line": { "type": "integer" },
"text": { "type": "string" }
}
}
},
"count": { "type": "integer" },
"truncated": { "type": "boolean" }
}
})),
idempotent: true,
cache_ttl_secs: None,
rate_limit: None,
}
}
pub fn list_dir() -> ToolSchema {
ToolSchema {
name: "list_dir".to_string(),
description: "List the immediate entries of a directory (NON-recursive), \
returning `{name, path, is_dir, size_bytes}` for each. Hidden \
dotfiles and `node_modules`/`__pycache__`/`target` are omitted. For \
a recursive or globbed search use find_files; to search file \
contents use grep_files."
.to_string(),
parameters: json!({
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Directory path"
}
},
"required": ["path"]
}),
returns: Some(json!({
"type": "object",
"properties": {
"entries": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": { "type": "string" },
"path": { "type": "string" },
"is_dir": { "type": "boolean" },
"size_bytes": { "type": "integer" }
}
}
}
}
})),
idempotent: true,
cache_ttl_secs: None,
rate_limit: None,
}
}
pub fn http_request() -> ToolSchema {
ToolSchema {
name: "http_request".to_string(),
description: "Make an HTTP request to a URL.".to_string(),
parameters: json!({
"type": "object",
"properties": {
"url": {
"type": "string",
"description": "The URL to request"
},
"method": {
"type": "string",
"description": "HTTP method (GET, POST, PUT, DELETE, PATCH)",
"enum": ["GET", "POST", "PUT", "DELETE", "PATCH"]
},
"headers": {
"type": "object",
"description": "Request headers as key-value pairs"
},
"body": {
"type": "string",
"description": "Request body (for POST/PUT/PATCH)"
}
},
"required": ["url"]
}),
returns: Some(json!({
"type": "object",
"properties": {
"status": { "type": "integer" },
"headers": { "type": "object" },
"body": { "type": "string" }
}
})),
idempotent: false,
cache_ttl_secs: None,
rate_limit: Some(ToolRateLimit {
max_calls: 30,
interval_secs: 60.0,
}),
}
}
pub fn calculate() -> ToolSchema {
ToolSchema {
name: "calculate".to_string(),
description: "Evaluate a mathematical expression exactly. Prefer this over \
arithmetic in your head or a shell one-liner whenever a number \
has to be right."
.to_string(),
parameters: json!({
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "Mathematical expression to evaluate. `^` is exponentiation (e.g. '2^3' = 8); also supports + - * / %, parentheses, and standard functions (e.g. sqrt, sin, ln). Example: '2 + 3 * 4'."
}
},
"required": ["expression"]
}),
returns: Some(json!({
"type": "object",
"properties": {
"result": { "type": "number" }
}
})),
idempotent: true,
cache_ttl_secs: Some(3600),
rate_limit: None,
}
}
pub fn search() -> ToolSchema {
ToolSchema {
name: "search".to_string(),
description: "Search for information using a query string.".to_string(),
parameters: json!({
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Search query"
},
"max_results": {
"type": "integer",
"description": "Maximum number of results (default: 10)"
}
},
"required": ["query"]
}),
returns: Some(json!({
"type": "object",
"properties": {
"results": {
"type": "array",
"items": {
"type": "object",
"properties": {
"title": { "type": "string" },
"snippet": { "type": "string" },
"url": { "type": "string" }
}
}
}
}
})),
idempotent: true,
cache_ttl_secs: Some(60),
rate_limit: Some(ToolRateLimit {
max_calls: 10,
interval_secs: 60.0,
}),
}
}
pub fn browser() -> ToolSchema {
ToolSchema {
name: "browser".to_string(),
description: "Navigate and interact with web pages in a browser.".to_string(),
parameters: json!({
"type": "object",
"properties": {
"action": {
"type": "string",
"description": "Browser action to perform",
"enum": ["navigate", "click", "fill", "screenshot", "text", "back", "forward"]
},
"url": {
"type": "string",
"description": "URL to navigate to (for 'navigate' action)"
},
"selector": {
"type": "string",
"description": "CSS selector for the target element"
},
"value": {
"type": "string",
"description": "Value to fill (for 'fill' action)"
}
},
"required": ["action"]
}),
returns: Some(json!({
"type": "object",
"properties": {
"success": { "type": "boolean" },
"content": { "type": "string" },
"screenshot_path": { "type": "string" }
}
})),
idempotent: false,
cache_ttl_secs: None,
rate_limit: Some(ToolRateLimit {
max_calls: 60,
interval_secs: 60.0,
}),
}
}
pub fn all() -> Vec<ToolSchema> {
vec![
shell(),
read_file(),
edit_file(),
write_file(),
list_dir(),
find_files(),
grep_files(),
http_request(),
calculate(),
search(),
browser(),
]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn all_schemas_have_required_fields() {
for schema in all() {
assert!(!schema.name.is_empty(), "schema name is empty");
assert!(
!schema.description.is_empty(),
"schema {} has no description",
schema.name
);
assert!(
schema.parameters.is_object(),
"schema {} parameters not an object",
schema.name
);
let params = schema.parameters.as_object().unwrap();
assert_eq!(
params.get("type").and_then(|v| v.as_str()),
Some("object"),
"schema {} parameters type not 'object'",
schema.name
);
assert!(
params.contains_key("properties"),
"schema {} parameters missing 'properties'",
schema.name
);
assert!(
params.contains_key("required"),
"schema {} parameters missing 'required'",
schema.name
);
}
}
#[test]
fn schemas_are_unique() {
let schemas = all();
let names: Vec<&str> = schemas.iter().map(|s| s.name.as_str()).collect();
let mut unique = names.clone();
unique.sort();
unique.dedup();
assert_eq!(names.len(), unique.len(), "duplicate schema names");
}
#[test]
fn idempotent_tools_have_cache_hints() {
for schema in all() {
if schema.idempotent && schema.name != "shell" {
}
}
assert!(calculate().cache_ttl_secs.is_some());
assert!(search().cache_ttl_secs.is_some());
for schema in [read_file(), find_files(), grep_files(), list_dir()] {
assert!(
schema.cache_ttl_secs.is_none(),
"{} must not be cached — a stale result would undermine the \
read/edit staleness contract",
schema.name
);
}
}
#[test]
fn rate_limited_tools() {
assert!(http_request().rate_limit.is_some());
assert!(search().rate_limit.is_some());
assert!(browser().rate_limit.is_some());
assert!(shell().rate_limit.is_none());
}
}