Skip to content

Adding Tools

Tools are how Codive interacts with your system. Codive comes with built-in tools for file operations, shell commands, and code search. You can extend Codive by adding custom tools for your specific workflows.

Codive includes these tools out of the box:

ToolDescription
readRead file contents
writeWrite or modify files
bashExecute shell commands
globFind files by pattern
grepSearch file contents

Tools are defined in Rust and implement the Tool trait.

use async_trait::async_trait;
use serde::{Deserialize, Serialize};
#[async_trait]
pub trait Tool: Send + Sync {
/// Tool name used in prompts
fn name(&self) -> &str;
/// Description shown to the AI
fn description(&self) -> &str;
/// JSON schema for parameters
fn parameters_schema(&self) -> serde_json::Value;
/// Execute the tool
async fn execute(
&self,
params: serde_json::Value,
context: &ToolContext,
) -> Result<ToolOutput, ToolError>;
}

Let’s create a tool that makes HTTP requests:

  1. Create the tool file

    Create src/tools/http.rs:

    use async_trait::async_trait;
    use reqwest::Client;
    use serde::{Deserialize, Serialize};
    use serde_json::json;
    use crate::tool::{Tool, ToolContext, ToolError, ToolOutput};
    pub struct HttpTool {
    client: Client,
    }
    impl HttpTool {
    pub fn new() -> Self {
    Self {
    client: Client::new(),
    }
    }
    }
    #[derive(Deserialize)]
    struct HttpParams {
    method: String,
    url: String,
    headers: Option<std::collections::HashMap<String, String>>,
    body: Option<String>,
    }
    #[async_trait]
    impl Tool for HttpTool {
    fn name(&self) -> &str {
    "http"
    }
    fn description(&self) -> &str {
    "Make HTTP requests to external APIs. \
    Supports GET, POST, PUT, DELETE methods."
    }
    fn parameters_schema(&self) -> serde_json::Value {
    json!({
    "type": "object",
    "required": ["method", "url"],
    "properties": {
    "method": {
    "type": "string",
    "enum": ["GET", "POST", "PUT", "DELETE"],
    "description": "HTTP method"
    },
    "url": {
    "type": "string",
    "description": "Request URL"
    },
    "headers": {
    "type": "object",
    "description": "Optional headers",
    "additionalProperties": { "type": "string" }
    },
    "body": {
    "type": "string",
    "description": "Request body (for POST/PUT)"
    }
    }
    })
    }
    async fn execute(
    &self,
    params: serde_json::Value,
    _context: &ToolContext,
    ) -> Result<ToolOutput, ToolError> {
    let params: HttpParams = serde_json::from_value(params)
    .map_err(|e| ToolError::InvalidParams(e.to_string()))?;
    let mut request = match params.method.as_str() {
    "GET" => self.client.get(&params.url),
    "POST" => self.client.post(&params.url),
    "PUT" => self.client.put(&params.url),
    "DELETE" => self.client.delete(&params.url),
    _ => return Err(ToolError::InvalidParams(
    format!("Unknown method: {}", params.method)
    )),
    };
    if let Some(headers) = params.headers {
    for (key, value) in headers {
    request = request.header(&key, &value);
    }
    }
    if let Some(body) = params.body {
    request = request.body(body);
    }
    let response = request
    .send()
    .await
    .map_err(|e| ToolError::Execution(e.to_string()))?;
    let status = response.status();
    let body = response
    .text()
    .await
    .map_err(|e| ToolError::Execution(e.to_string()))?;
    Ok(ToolOutput {
    content: format!(
    "Status: {}\n\nBody:\n{}",
    status,
    body
    ),
    metadata: Some(json!({
    "status_code": status.as_u16()
    })),
    })
    }
    }
  2. Register the tool

    In src/tools/mod.rs:

    mod http;
    pub use http::HttpTool;
    pub fn all_tools() -> Vec<Box<dyn Tool>> {
    vec![
    Box::new(ReadTool::new()),
    Box::new(WriteTool::new()),
    Box::new(BashTool::new()),
    Box::new(GlobTool::new()),
    Box::new(GrepTool::new()),
    Box::new(HttpTool::new()), // Add our new tool
    ]
    }
  3. Use the tool

    Now you can ask Codive:

    > fetch the JSON from https://api.example.com/users

    Codive will use your HTTP tool to make the request.

Write descriptions that help the AI understand when to use the tool:

fn description(&self) -> &str {
"Search for files in the project directory using glob patterns. \
Use this when you need to find files by name or extension. \
Examples: '**/*.rs' finds all Rust files, 'src/**/*.test.ts' \
finds all test files in src."
}

Define parameter schemas precisely:

fn parameters_schema(&self) -> serde_json::Value {
json!({
"type": "object",
"required": ["pattern"], // Mark required fields
"properties": {
"pattern": {
"type": "string",
"description": "Glob pattern (e.g., '**/*.rs')"
},
"path": {
"type": "string",
"description": "Optional starting directory",
"default": "." // Document defaults
}
}
})
}

Provide helpful error messages:

async fn execute(&self, params: Value, ctx: &ToolContext) -> Result<ToolOutput, ToolError> {
let path = params["path"]
.as_str()
.ok_or_else(|| ToolError::InvalidParams(
"Missing required parameter 'path'. \
Provide the file path to read."
.to_string()
))?;
// Validate the path exists
if !std::path::Path::new(path).exists() {
return Err(ToolError::Execution(
format!("File not found: {}. \
Check the path and try again.", path)
));
}
// ... rest of implementation
}

Implement safety checks:

async fn execute(&self, params: Value, ctx: &ToolContext) -> Result<ToolOutput, ToolError> {
let command = params["command"].as_str().unwrap();
// Block dangerous commands
let blocked = ["rm -rf /", "sudo", "> /dev/"];
for pattern in blocked {
if command.contains(pattern) {
return Err(ToolError::Execution(
format!("Command blocked for safety: {}", command)
));
}
}
// Proceed with execution
// ...
}

Configure tools in codive.toml:

[tools]
enabled = ["read", "write", "bash", "glob", "grep", "http"]
[tools.http]
# Custom configuration for HTTP tool
timeout = 30
max_redirects = 5
allowed_domains = ["api.example.com", "api.github.com"]

Access configuration in your tool:

pub struct HttpTool {
client: Client,
config: HttpConfig,
}
impl HttpTool {
pub fn from_config(config: &Config) -> Self {
let http_config = config.tools.http.clone();
Self {
client: Client::builder()
.timeout(Duration::from_secs(http_config.timeout))
.build()
.unwrap(),
config: http_config,
}
}
}
pub struct DatabaseTool {
pool: PgPool,
}
#[async_trait]
impl Tool for DatabaseTool {
fn name(&self) -> &str { "database" }
fn description(&self) -> &str {
"Execute read-only SQL queries against the database."
}
async fn execute(&self, params: Value, _ctx: &ToolContext) -> Result<ToolOutput, ToolError> {
let query = params["query"].as_str().unwrap();
// Only allow SELECT queries
if !query.trim().to_uppercase().starts_with("SELECT") {
return Err(ToolError::Execution(
"Only SELECT queries are allowed".to_string()
));
}
let rows: Vec<serde_json::Value> = sqlx::query(query)
.fetch_all(&self.pool)
.await
.map_err(|e| ToolError::Execution(e.to_string()))?
.iter()
.map(|row| row_to_json(row))
.collect();
Ok(ToolOutput {
content: serde_json::to_string_pretty(&rows).unwrap(),
metadata: None,
})
}
}
pub struct ApiDocTool {
openapi_spec: OpenApiSpec,
}
#[async_trait]
impl Tool for ApiDocTool {
fn name(&self) -> &str { "api_docs" }
fn description(&self) -> &str {
"Search and retrieve API documentation. \
Use to find endpoint details, request/response schemas."
}
async fn execute(&self, params: Value, _ctx: &ToolContext) -> Result<ToolOutput, ToolError> {
let query = params["query"].as_str().unwrap();
let results = self.openapi_spec.search(query);
Ok(ToolOutput {
content: format_results(&results),
metadata: None,
})
}
}