pub mod find_package;
pub mod list_dir;
pub mod read_file;
pub mod retrieve_observation;
pub mod search_text;
pub mod web_search;
use crate::{
common::{
package_resolver::PackageResolverError,
path_traversal::{DirectoryEntryKind, PathTraversalError},
},
config::{self},
};
use rmcp::{
RoleClient, ServiceExt,
model::{ClientCapabilities, ClientInfo, Implementation, Tool as RmcpTool},
service::{ClientInitializeError, RunningService},
transport::StreamableHttpClientTransport,
};
use serde::{Deserialize, Serialize};
use std::{
collections::BTreeMap,
io::ErrorKind,
path::{Path, PathBuf},
};
#[derive(Clone, Debug, Deserialize, Serialize)]
struct Range {
path: PathBuf,
start: usize,
end: usize,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct FileContent {
range: Range,
content: String,
information: String,
}
pub fn io_error_to_tool_error(err: std::io::Error, path: &Path) -> ToolError {
let path_string = path.to_string_lossy().to_string();
match err.kind() {
ErrorKind::NotFound => ToolError::DirectoryEntryNotFound {
path: path_string,
kind: DirectoryEntryKind::File,
suggestion: "Use list_dir tool to explore available files.".to_string(),
},
ErrorKind::PermissionDenied => ToolError::PermissionDenied { path: path_string },
_ => ToolError::Io {
path: Some(path_string),
source: err,
},
}
}
#[derive(Debug)]
pub enum ToolError {
DirectoryEntryNotFound {
path: String,
kind: DirectoryEntryKind,
suggestion: String,
},
NetworkError {
reason: String,
},
NotAFile {
path: String,
suggestion: String,
},
PackageNotFound {
identifier: String,
},
PackageSourceNotAvailable {
identifier: String,
},
PathTraversal {
requested: String,
base: String,
},
PermissionDenied {
path: String,
},
ObservationNotFound {
identifier: String,
},
Io {
path: Option<String>,
source: std::io::Error,
},
Mcp {
kind: String,
info: String,
}, }
impl std::fmt::Display for ToolError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ToolError::DirectoryEntryNotFound {
path,
kind,
suggestion,
} => {
write!(f, "Error: {} '{}' not found. {}", kind, path, suggestion)
}
ToolError::NetworkError { reason } => {
write!(f, "Error: '{}' when making a network request.", reason)
}
ToolError::NotAFile { path, suggestion } => {
write!(f, "Error: '{}' is not a file. {}", path, suggestion)
}
ToolError::PackageNotFound { identifier } => {
write!(
f,
"Error: Package with identifier '{}' not found. \
Packages follow the pattern '<pacman>:<name>@<version>'. \
Use the find_package tool to get available package identifiers.",
identifier
)
}
ToolError::PackageSourceNotAvailable { identifier } => {
write!(
f,
"Error: The source code for the package with identifier '{}' is not avaialbe locally. \
If possible, continue without knowledge from the source code or try to find the required source code online.",
identifier
)
}
ToolError::PathTraversal { requested, base } => {
write!(
f,
"Error: Path '{}' is outside the allowed directory '{}'. \
Only paths within the project root are accessible.",
requested, base
)
}
ToolError::PermissionDenied { path } => {
write!(
f,
"Error: Permission denied when accessing '{}'. \
This file/directory requires elevated permissions. \
You cannot access this resource. Continue with available data \
or ask the user to check permissions.",
path
)
}
ToolError::ObservationNotFound { identifier } => {
write!(
f,
"Error: Observation with identifier '{}' not found. \
Check the conversation history for valid observation IDs.",
identifier
)
}
ToolError::Io { path, source } => {
if let Some(p) = path {
write!(f, "Error reading '{}': {}", p, source)
} else {
write!(f, "Error: {}", source)
}
}
ToolError::Mcp { kind, info } => {
write!(f, "Error {} accessing MCP with info '{}'.", kind, info)
}
}
}
}
impl std::error::Error for ToolError {}
impl From<PathTraversalError> for ToolError {
fn from(value: PathTraversalError) -> Self {
match value {
PathTraversalError::OutOfScope { requested, base } => {
ToolError::PathTraversal { requested, base }
}
PathTraversalError::NotFound { path, kind } => ToolError::DirectoryEntryNotFound {
path,
kind,
suggestion: "Use list_dir tool to find files and directories.".to_string(),
},
PathTraversalError::Io { path, source } => ToolError::Io { path, source },
PathTraversalError::PermissionDenied { path } => ToolError::PermissionDenied { path },
}
}
}
impl From<PackageResolverError> for ToolError {
fn from(value: PackageResolverError) -> Self {
match value {
PackageResolverError::PackageNotFound { identifier } => {
ToolError::PackageNotFound { identifier }
}
PackageResolverError::SourceNotAvailable { identifier } => {
ToolError::PackageSourceNotAvailable { identifier }
}
}
}
}
pub async fn get_mcp_tools(
tools: &BTreeMap<String, config::McpTool>,
) -> Vec<(RunningService<RoleClient, ClientInfo>, Vec<RmcpTool>)> {
let mut result = Vec::new();
for (name, mcp_tool) in tools {
if let Ok((peer, tools)) = connect_and_fetch_tools(mcp_tool).await {
result.push((peer, tools));
} else {
}
}
result
}
async fn connect_and_fetch_tools(
mcp_tool: &config::McpTool,
) -> Result<(RunningService<RoleClient, ClientInfo>, Vec<RmcpTool>), anyhow::Error> {
let client = create_client(mcp_tool).await?;
let tools = client.list_tools(Default::default()).await?.tools;
Ok((client, tools))
}
async fn create_client(
mcp_tool: &config::McpTool,
) -> Result<RunningService<RoleClient, ClientInfo>, Box<ClientInitializeError>> {
let transport = StreamableHttpClientTransport::from_uri(mcp_tool.url.clone());
let client_info_impl = Implementation::new(
env!("CARGO_PKG_NAME").to_string(),
env!("CARGO_PKG_VERSION").to_string(),
);
let client_info = ClientInfo::new(ClientCapabilities::default(), client_info_impl);
client_info.serve(transport).await.map_err(|e| {
Box::new(e)
})
}
fn ignore_description(hidden: bool, ignore: bool) -> String {
if hidden && ignore {
" Hidden files (dotfiles) and files matching .gitignore are excluded."
} else if hidden {
" Hidden files (dotfiles) are excluded."
} else if ignore {
" Files matching .gitignore are excluded."
} else {
""
}
.to_string()
}