feat: ToolError migration to ErrorData (#4051)

This commit is contained in:
Alex Hancock
2025-08-12 16:18:41 -04:00
committed by GitHub
parent 88b013194c
commit bd1eff52a4
35 changed files with 2459 additions and 1336 deletions
Generated
+1 -1
View File
@@ -3519,7 +3519,7 @@ dependencies = [
[[package]]
name = "goose-test"
version = "1.1.0"
version = "1.3.0"
dependencies = [
"clap",
"serde_json",
@@ -2,11 +2,10 @@
//! add a tool you want to have around and then add the client to the extension router
use mcp_client::client::{Error, McpClientTrait};
use mcp_core::ToolError;
use rmcp::{
model::{
CallToolResult, Content, GetPromptResult, ListPromptsResult, ListResourcesResult,
ListToolsResult, ReadResourceResult, ServerNotification, Tool,
CallToolResult, Content, ErrorData, GetPromptResult, ListPromptsResult,
ListResourcesResult, ListToolsResult, ReadResourceResult, ServerNotification, Tool,
},
object,
};
@@ -17,7 +16,7 @@ use tokio_util::sync::CancellationToken;
pub struct MockClient {
tools: HashMap<String, Tool>,
handlers: HashMap<String, Box<dyn Fn(&Value) -> Result<Vec<Content>, ToolError> + Send + Sync>>,
handlers: HashMap<String, Box<dyn Fn(&Value) -> Result<Vec<Content>, ErrorData> + Send + Sync>>,
}
impl MockClient {
@@ -30,7 +29,7 @@ impl MockClient {
pub(crate) fn add_tool<F>(mut self, tool: Tool, handler: F) -> Self
where
F: Fn(&Value) -> Result<Vec<Content>, ToolError> + Send + Sync + 'static,
F: Fn(&Value) -> Result<Vec<Content>, ErrorData> + Send + Sync + 'static,
{
let tool_name = tool.name.to_string();
self.tools.insert(tool_name.clone(), tool);
+7 -3
View File
@@ -34,9 +34,9 @@ use goose::config::Config;
use goose::providers::pricing::initialize_pricing_cache;
use goose::session;
use input::InputResult;
use mcp_core::handler::ToolError;
use rmcp::model::PromptMessage;
use rmcp::model::ServerNotification;
use rmcp::model::{ErrorCode, ErrorData};
use goose::conversation::message::{Message, MessageContent};
use rand::{distributions::Alphanumeric, Rng};
@@ -927,7 +927,7 @@ impl Session {
let mut response_message = Message::user();
response_message.content.push(MessageContent::tool_response(
confirmation.id.clone(),
Err(ToolError::ExecutionError("Tool call cancelled by user".to_string()))
Err(ErrorData { code: ErrorCode::INVALID_REQUEST, message: std::borrow::Cow::from("Tool call cancelled by user".to_string()), data: None })
));
self.messages.push(response_message);
if let Some(session_file) = &self.session_file {
@@ -1294,7 +1294,11 @@ impl Session {
for (req_id, _) in &tool_requests {
response_message.content.push(MessageContent::tool_response(
req_id.clone(),
Err(ToolError::ExecutionError(notification.clone())),
Err(ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: std::borrow::Cow::from(notification.clone()),
data: None,
}),
));
}
self.push_message(response_message);
@@ -1,7 +1,7 @@
use docx_rs::*;
use image::{self, ImageFormat};
use mcp_core::ToolError;
use rmcp::model::Content;
use rmcp::model::{Content, ErrorCode, ErrorData};
use std::borrow::Cow;
use std::{fs, io::Cursor};
#[derive(Debug)]
@@ -90,15 +90,19 @@ pub async fn docx_tool(
operation: &str,
content: Option<&str>,
params: Option<&serde_json::Value>,
) -> Result<Vec<Content>, ToolError> {
) -> Result<Vec<Content>, ErrorData> {
match operation {
"extract_text" => {
let file = fs::read(path).map_err(|e| {
ToolError::ExecutionError(format!("Failed to read DOCX file: {}", e))
let file = fs::read(path).map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!("Failed to read DOCX file: {}", e)),
data: None,
})?;
let docx = read_docx(&file).map_err(|e| {
ToolError::ExecutionError(format!("Failed to parse DOCX file: {}", e))
let docx = read_docx(&file).map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!("Failed to parse DOCX file: {}", e)),
data: None,
})?;
let mut text = String::new();
@@ -169,10 +173,10 @@ pub async fn docx_tool(
}
"update_doc" => {
let content = content.ok_or_else(|| {
ToolError::InvalidParameters(
"Content parameter required for update_doc".to_string(),
)
let content = content.ok_or_else(|| ErrorData {
code: ErrorCode::INVALID_PARAMS,
message: Cow::from("Content parameter required for update_doc"),
data: None,
})?;
// Parse update mode and style from params
@@ -186,15 +190,14 @@ pub async fn docx_tool(
let mode = match mode {
"append" => UpdateMode::Append,
"replace" => {
let old_text =
params
.get("old_text")
.and_then(|v| v.as_str())
.ok_or_else(|| {
ToolError::InvalidParameters(
"old_text parameter required for replace mode".to_string(),
)
})?;
let old_text = params
.get("old_text")
.and_then(|v| v.as_str())
.ok_or_else(|| ErrorData {
code: ErrorCode::INVALID_PARAMS,
message: Cow::from("old_text parameter required for replace mode"),
data: None,
})?;
UpdateMode::Replace {
old_text: old_text.to_string(),
}
@@ -213,10 +216,10 @@ pub async fn docx_tool(
let image_path = params
.get("image_path")
.and_then(|v| v.as_str())
.ok_or_else(|| {
ToolError::InvalidParameters(
"image_path parameter required for add_image mode".to_string(),
)
.ok_or_else(|| ErrorData {
code: ErrorCode::INVALID_PARAMS,
message: Cow::from("image_path parameter required for add_image mode"),
data: None,
})?
.to_string();
@@ -236,10 +239,11 @@ pub async fn docx_tool(
height,
}
}
_ => return Err(ToolError::InvalidParameters(
"Invalid mode. Must be 'append', 'replace', 'structured', or 'add_image'"
.to_string(),
)),
_ => return Err(ErrorData {
code: ErrorCode::INVALID_PARAMS,
message: Cow::from("Invalid mode. Must be 'append', 'replace', 'structured', or 'add_image'"),
data: None,
}),
};
(mode, style)
} else {
@@ -250,11 +254,15 @@ pub async fn docx_tool(
UpdateMode::Append => {
// Read existing document if it exists, or create new one
let mut doc = if std::path::Path::new(path).exists() {
let file = fs::read(path).map_err(|e| {
ToolError::ExecutionError(format!("Failed to read DOCX file: {}", e))
let file = fs::read(path).map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!("Failed to read DOCX file: {}", e)),
data: None,
})?;
read_docx(&file).map_err(|e| {
ToolError::ExecutionError(format!("Failed to parse DOCX file: {}", e))
read_docx(&file).map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!("Failed to parse DOCX file: {}", e)),
data: None,
})?
} else {
Docx::new()
@@ -278,13 +286,17 @@ pub async fn docx_tool(
let mut buf = Vec::new();
{
let mut cursor = Cursor::new(&mut buf);
doc.build().pack(&mut cursor).map_err(|e| {
ToolError::ExecutionError(format!("Failed to build DOCX: {}", e))
doc.build().pack(&mut cursor).map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!("Failed to build DOCX: {}", e)),
data: None,
})?;
}
fs::write(path, &buf).map_err(|e| {
ToolError::ExecutionError(format!("Failed to write DOCX file: {}", e))
fs::write(path, &buf).map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!("Failed to write DOCX file: {}", e)),
data: None,
})?;
Ok(vec![Content::text(format!(
@@ -295,12 +307,16 @@ pub async fn docx_tool(
UpdateMode::Replace { old_text } => {
// Read existing document
let file = fs::read(path).map_err(|e| {
ToolError::ExecutionError(format!("Failed to read DOCX file: {}", e))
let file = fs::read(path).map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!("Failed to read DOCX file: {}", e)),
data: None,
})?;
let docx = read_docx(&file).map_err(|e| {
ToolError::ExecutionError(format!("Failed to parse DOCX file: {}", e))
let docx = read_docx(&file).map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!("Failed to parse DOCX file: {}", e)),
data: None,
})?;
let mut new_doc = Docx::new();
@@ -371,22 +387,30 @@ pub async fn docx_tool(
}
if !found_text {
return Err(ToolError::ExecutionError(format!(
"Could not find text to replace: {}",
old_text
)));
return Err(ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!(
"Could not find text to replace: {}",
old_text
)),
data: None,
});
}
let mut buf = Vec::new();
{
let mut cursor = Cursor::new(&mut buf);
new_doc.build().pack(&mut cursor).map_err(|e| {
ToolError::ExecutionError(format!("Failed to build DOCX: {}", e))
new_doc.build().pack(&mut cursor).map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!("Failed to build DOCX: {}", e)),
data: None,
})?;
}
fs::write(path, &buf).map_err(|e| {
ToolError::ExecutionError(format!("Failed to write DOCX file: {}", e))
fs::write(path, &buf).map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!("Failed to write DOCX file: {}", e)),
data: None,
})?;
Ok(vec![Content::text(format!(
@@ -397,11 +421,15 @@ pub async fn docx_tool(
UpdateMode::InsertStructured { level, style } => {
let mut doc = if std::path::Path::new(path).exists() {
let file = fs::read(path).map_err(|e| {
ToolError::ExecutionError(format!("Failed to read DOCX file: {}", e))
let file = fs::read(path).map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!("Failed to read DOCX file: {}", e)),
data: None,
})?;
read_docx(&file).map_err(|e| {
ToolError::ExecutionError(format!("Failed to parse DOCX file: {}", e))
read_docx(&file).map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!("Failed to parse DOCX file: {}", e)),
data: None,
})?
} else {
Docx::new()
@@ -431,13 +459,17 @@ pub async fn docx_tool(
let mut buf = Vec::new();
{
let mut cursor = Cursor::new(&mut buf);
doc.build().pack(&mut cursor).map_err(|e| {
ToolError::ExecutionError(format!("Failed to build DOCX: {}", e))
doc.build().pack(&mut cursor).map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!("Failed to build DOCX: {}", e)),
data: None,
})?;
}
fs::write(path, &buf).map_err(|e| {
ToolError::ExecutionError(format!("Failed to write DOCX file: {}", e))
fs::write(path, &buf).map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!("Failed to write DOCX file: {}", e)),
data: None,
})?;
Ok(vec![Content::text(format!(
@@ -452,43 +484,55 @@ pub async fn docx_tool(
height,
} => {
let mut doc = if std::path::Path::new(path).exists() {
let file = fs::read(path).map_err(|e| {
ToolError::ExecutionError(format!("Failed to read DOCX file: {}", e))
let file = fs::read(path).map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!("Failed to read DOCX file: {}", e)),
data: None,
})?;
read_docx(&file).map_err(|e| {
ToolError::ExecutionError(format!("Failed to parse DOCX file: {}", e))
read_docx(&file).map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!("Failed to parse DOCX file: {}", e)),
data: None,
})?
} else {
Docx::new()
};
// Read the image file
let image_data = fs::read(&image_path).map_err(|e| {
ToolError::ExecutionError(format!("Failed to read image file: {}", e))
let image_data = fs::read(&image_path).map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!("Failed to read image file: {}", e)),
data: None,
})?;
// Get image format and extension
let extension = std::path::Path::new(&image_path)
.extension()
.and_then(|e| e.to_str())
.ok_or_else(|| {
ToolError::ExecutionError("Invalid image file extension".to_string())
.ok_or_else(|| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from("Invalid image file extension".to_string()),
data: None,
})?
.to_lowercase();
// Convert to PNG if not already PNG
let image_data = if extension != "png" {
// Try to convert to PNG using the image crate
let img = image::load_from_memory(&image_data).map_err(|e| {
ToolError::ExecutionError(format!("Failed to load image: {}", e))
let img = image::load_from_memory(&image_data).map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!("Failed to load image: {}", e)),
data: None,
})?;
let mut png_data = Vec::new();
img.write_to(&mut Cursor::new(&mut png_data), ImageFormat::Png)
.map_err(|e| {
ToolError::ExecutionError(format!(
.map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!(
"Failed to convert image to PNG: {}",
e
))
)),
data: None,
})?;
png_data
} else {
@@ -526,13 +570,17 @@ pub async fn docx_tool(
let mut buf = Vec::new();
{
let mut cursor = Cursor::new(&mut buf);
doc.build().pack(&mut cursor).map_err(|e| {
ToolError::ExecutionError(format!("Failed to build DOCX: {}", e))
doc.build().pack(&mut cursor).map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!("Failed to build DOCX: {}", e)),
data: None,
})?;
}
fs::write(path, &buf).map_err(|e| {
ToolError::ExecutionError(format!("Failed to write DOCX file: {}", e))
fs::write(path, &buf).map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!("Failed to write DOCX file: {}", e)),
data: None,
})?;
Ok(vec![Content::text(format!(
@@ -543,10 +591,14 @@ pub async fn docx_tool(
}
}
_ => Err(ToolError::InvalidParameters(format!(
"Invalid operation: {}. Valid operations are: 'extract_text', 'update_doc'",
operation
))),
_ => Err(ErrorData {
code: ErrorCode::INVALID_PARAMS,
message: Cow::from(format!(
"Invalid operation: {}. Valid operations are: 'extract_text', 'update_doc'",
operation
)),
data: None,
}),
}
}
+377 -172
View File
@@ -3,6 +3,7 @@ use etcetera::{choose_app_strategy, AppStrategy};
use indoc::{formatdoc, indoc};
use reqwest::{Client, Url};
use serde_json::Value;
use std::borrow::Cow;
use std::{
collections::HashMap, fs, future::Future, path::PathBuf, pin::Pin, sync::Arc, sync::Mutex,
};
@@ -12,15 +13,14 @@ use tokio::{process::Command, sync::mpsc};
use std::os::unix::fs::PermissionsExt;
use mcp_core::{
handler::{
require_str_parameter, require_u64_parameter, PromptError, ResourceError, ToolError,
},
handler::{require_str_parameter, require_u64_parameter, PromptError, ResourceError},
protocol::ServerCapabilities,
};
use mcp_server::router::CapabilitiesBuilder;
use mcp_server::Router;
use rmcp::model::{
AnnotateAble, Content, JsonRpcMessage, Prompt, RawResource, Resource, Tool, ToolAnnotations,
AnnotateAble, Content, ErrorCode, ErrorData, JsonRpcMessage, Prompt, RawResource, Resource,
Tool, ToolAnnotations,
};
use rmcp::object;
@@ -570,17 +570,24 @@ impl ComputerControllerRouter {
content: &[u8],
prefix: &str,
extension: &str,
) -> Result<PathBuf, ToolError> {
) -> Result<PathBuf, ErrorData> {
let cache_path = self.get_cache_path(prefix, extension);
fs::write(&cache_path, content)
.map_err(|e| ToolError::ExecutionError(format!("Failed to write to cache: {}", e)))?;
fs::write(&cache_path, content).map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!("Failed to write to cache: {}", e)),
data: None,
})?;
Ok(cache_path)
}
// Helper function to register a file as a resource
fn register_as_resource(&self, cache_path: &PathBuf, mime_type: &str) -> Result<(), ToolError> {
fn register_as_resource(&self, cache_path: &PathBuf, mime_type: &str) -> Result<(), ErrorData> {
let uri = Url::from_file_path(cache_path)
.map_err(|_| ToolError::ExecutionError("Invalid cache path".into()))?
.map_err(|_| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from("Invalid cache path"),
data: None,
})?
.to_string();
let mut resource = RawResource::new(uri.clone(), cache_path.to_string_lossy().into_owned());
@@ -596,8 +603,16 @@ impl ComputerControllerRouter {
Ok(())
}
async fn web_scrape(&self, params: Value) -> Result<Vec<Content>, ToolError> {
let url = require_str_parameter(&params, "url")?;
async fn web_scrape(&self, params: Value) -> Result<Vec<Content>, ErrorData> {
let url = params
.get("url")
.and_then(|v| v.as_str())
.ok_or_else(|| ErrorData {
code: ErrorCode::INVALID_PARAMS,
message: Cow::from("Missing 'url' parameter"),
data: None,
})?;
let save_as = params
.get("save_as")
.and_then(|v| v.as_str())
@@ -609,48 +624,64 @@ impl ComputerControllerRouter {
.get(url)
.send()
.await
.map_err(|e| ToolError::ExecutionError(format!("Failed to fetch URL: {}", e)))?;
.map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!("Failed to fetch URL: {}", e)),
data: None,
})?;
let status = response.status();
if !status.is_success() {
return Err(ToolError::ExecutionError(format!(
"HTTP request failed with status: {}",
status
)));
return Err(ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!("HTTP request failed with status: {}", status)),
data: None,
});
}
// Process based on save_as parameter
let (content, extension) =
match save_as {
"text" => {
let text = response.text().await.map_err(|e| {
ToolError::ExecutionError(format!("Failed to get text: {}", e))
})?;
(text.into_bytes(), "txt")
}
"json" => {
let text = response.text().await.map_err(|e| {
ToolError::ExecutionError(format!("Failed to get text: {}", e))
})?;
// Verify it's valid JSON
serde_json::from_str::<Value>(&text).map_err(|e| {
ToolError::ExecutionError(format!("Invalid JSON response: {}", e))
})?;
(text.into_bytes(), "json")
}
"binary" => {
let bytes = response.bytes().await.map_err(|e| {
ToolError::ExecutionError(format!("Failed to get bytes: {}", e))
})?;
(bytes.to_vec(), "bin")
}
_ => {
return Err(ToolError::InvalidParameters(format!(
"Invalid 'save_as' parameter: {}. Valid options are: 'text', 'json', 'binary'",
save_as
)));
}
};
let (content, extension) = match save_as {
"text" => {
let text = response.text().await.map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!("Failed to get text: {}", e)),
data: None,
})?;
(text.into_bytes(), "txt")
}
"json" => {
let text = response.text().await.map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!("Failed to get text: {}", e)),
data: None,
})?;
// Verify it's valid JSON
serde_json::from_str::<Value>(&text).map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!("Invalid JSON response: {}", e)),
data: None,
})?;
(text.into_bytes(), "json")
}
"binary" => {
let bytes = response.bytes().await.map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!("Failed to get bytes: {}", e)),
data: None,
})?;
(bytes.to_vec(), "bin")
}
_ => {
return Err(ErrorData {
code: ErrorCode::INVALID_PARAMS,
message: Cow::from(format!(
"Invalid 'save_as' parameter: {}. Valid options are: 'text', 'json', 'binary'",
save_as
)),
data: None,
});
}
};
// Save to cache
let cache_path = self.save_to_cache(&content, "web", extension).await?;
@@ -665,16 +696,24 @@ impl ComputerControllerRouter {
}
// Implement quick_script tool functionality
async fn quick_script(&self, params: Value) -> Result<Vec<Content>, ToolError> {
async fn quick_script(&self, params: Value) -> Result<Vec<Content>, ErrorData> {
let language = params
.get("language")
.and_then(|v| v.as_str())
.ok_or_else(|| ToolError::InvalidParameters("Missing 'language' parameter".into()))?;
.ok_or_else(|| ErrorData {
code: ErrorCode::INVALID_PARAMS,
message: Cow::from("Missing 'language' parameter"),
data: None,
})?;
let script = params
.get("script")
.and_then(|v| v.as_str())
.ok_or_else(|| ToolError::InvalidParameters("Missing 'script' parameter".into()))?;
.ok_or_else(|| ErrorData {
code: ErrorCode::INVALID_PARAMS,
message: Cow::from("Missing 'script' parameter"),
data: None,
})?;
let save_output = params
.get("save_output")
@@ -682,8 +721,10 @@ impl ComputerControllerRouter {
.unwrap_or(false);
// Create a temporary directory for the script
let script_dir = tempfile::tempdir().map_err(|e| {
ToolError::ExecutionError(format!("Failed to create temporary directory: {}", e))
let script_dir = tempfile::tempdir().map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!("Failed to create temporary directory: {}", e)),
data: None,
})?;
let (shell, shell_arg) = self.system_automation.get_shell_command();
@@ -694,24 +735,27 @@ impl ComputerControllerRouter {
"script.{}",
if cfg!(windows) { "bat" } else { "sh" }
));
fs::write(&script_path, script).map_err(|e| {
ToolError::ExecutionError(format!("Failed to write script: {}", e))
fs::write(&script_path, script).map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!("Failed to write script: {}", e)),
data: None,
})?;
// Set execute permissions on Unix systems
#[cfg(unix)]
{
let mut perms = fs::metadata(&script_path)
.map_err(|e| {
ToolError::ExecutionError(format!("Failed to get file metadata: {}", e))
.map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!("Failed to get file metadata: {}", e)),
data: None,
})?
.permissions();
perms.set_mode(0o755); // rwxr-xr-x
fs::set_permissions(&script_path, perms).map_err(|e| {
ToolError::ExecutionError(format!(
"Failed to set execute permissions: {}",
e
))
fs::set_permissions(&script_path, perms).map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!("Failed to set execute permissions: {}", e)),
data: None,
})?;
}
@@ -719,24 +763,30 @@ impl ComputerControllerRouter {
}
"ruby" => {
let script_path = script_dir.path().join("script.rb");
fs::write(&script_path, script).map_err(|e| {
ToolError::ExecutionError(format!("Failed to write script: {}", e))
fs::write(&script_path, script).map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!("Failed to write script: {}", e)),
data: None,
})?;
format!("ruby {}", script_path.display())
}
"powershell" => {
let script_path = script_dir.path().join("script.ps1");
fs::write(&script_path, script).map_err(|e| {
ToolError::ExecutionError(format!("Failed to write script: {}", e))
fs::write(&script_path, script).map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!("Failed to write script: {}", e)),
data: None,
})?;
script_path.display().to_string()
}
_ => {
return Err( ToolError::InvalidParameters(
format!("Invalid 'language' parameter: {}. Valid options are: 'shell', 'batch', 'ruby', 'powershell", language)
));
return Err(ErrorData {
code: ErrorCode::INVALID_PARAMS,
message: Cow::from(format!("Invalid 'language' parameter: {}. Valid options are: 'shell', 'batch', 'ruby', 'powershell'", language)),
data: None,
});
}
};
@@ -752,8 +802,10 @@ impl ComputerControllerRouter {
.env("GOOSE_TERMINAL", "1")
.output()
.await
.map_err(|e| {
ToolError::ExecutionError(format!("Failed to run script: {}", e))
.map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!("Failed to run script: {}", e)),
data: None,
})?
}
_ => Command::new(shell)
@@ -762,7 +814,11 @@ impl ComputerControllerRouter {
.env("GOOSE_TERMINAL", "1")
.output()
.await
.map_err(|e| ToolError::ExecutionError(format!("Failed to run script: {}", e)))?,
.map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!("Failed to run script: {}", e)),
data: None,
})?,
};
let output_str = String::from_utf8_lossy(&output.stdout).into_owned();
@@ -792,11 +848,15 @@ impl ComputerControllerRouter {
}
// Implement computer control functionality
async fn computer_control(&self, params: Value) -> Result<Vec<Content>, ToolError> {
async fn computer_control(&self, params: Value) -> Result<Vec<Content>, ErrorData> {
let script = params
.get("script")
.and_then(|v| v.as_str())
.ok_or_else(|| ToolError::InvalidParameters("Missing 'script' parameter".into()))?;
.ok_or_else(|| ErrorData {
code: ErrorCode::INVALID_PARAMS,
message: Cow::from("Missing 'script' parameter"),
data: None,
})?;
let save_output = params
.get("save_output")
@@ -807,7 +867,11 @@ impl ComputerControllerRouter {
let output = self
.system_automation
.execute_system_script(script)
.map_err(|e| ToolError::ExecutionError(format!("Failed to execute script: {}", e)))?;
.map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!("Failed to execute script: {}", e)),
data: None,
})?;
let mut result = format!("Script completed successfully.\n\nOutput:\n{}", output);
@@ -825,71 +889,110 @@ impl ComputerControllerRouter {
Ok(vec![Content::text(result)])
}
async fn xlsx_tool(&self, params: Value) -> Result<Vec<Content>, ToolError> {
async fn xlsx_tool(&self, params: Value) -> Result<Vec<Content>, ErrorData> {
let path = params
.get("path")
.and_then(|v| v.as_str())
.ok_or_else(|| ToolError::InvalidParameters("Missing 'path' parameter".into()))?;
.ok_or_else(|| ErrorData {
code: ErrorCode::INVALID_PARAMS,
message: Cow::from("Missing 'path' parameter"),
data: None,
})?;
let operation = params
.get("operation")
.and_then(|v| v.as_str())
.ok_or_else(|| ToolError::InvalidParameters("Missing 'operation' parameter".into()))?;
.ok_or_else(|| ErrorData {
code: ErrorCode::INVALID_PARAMS,
message: Cow::from("Missing 'operation' parameter"),
data: None,
})?;
match operation {
"list_worksheets" => {
let xlsx = xlsx_tool::XlsxTool::new(path)
.map_err(|e| ToolError::ExecutionError(e.to_string()))?;
let worksheets = xlsx
.list_worksheets()
.map_err(|e| ToolError::ExecutionError(e.to_string()))?;
let xlsx = xlsx_tool::XlsxTool::new(path).map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(e.to_string()),
data: None,
})?;
let worksheets = xlsx.list_worksheets().map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(e.to_string()),
data: None,
})?;
Ok(vec![Content::text(format!("{:#?}", worksheets))])
}
"get_columns" => {
let xlsx = xlsx_tool::XlsxTool::new(path)
.map_err(|e| ToolError::ExecutionError(e.to_string()))?;
let xlsx = xlsx_tool::XlsxTool::new(path).map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(e.to_string()),
data: None,
})?;
let worksheet = if let Some(name) = params.get("worksheet").and_then(|v| v.as_str())
{
xlsx.get_worksheet_by_name(name)
.map_err(|e| ToolError::ExecutionError(e.to_string()))?
xlsx.get_worksheet_by_name(name).map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(e.to_string()),
data: None,
})?
} else {
xlsx.get_worksheet_by_index(0)
.map_err(|e| ToolError::ExecutionError(e.to_string()))?
xlsx.get_worksheet_by_index(0).map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(e.to_string()),
data: None,
})?
};
let columns = xlsx
.get_column_names(worksheet)
.map_err(|e| ToolError::ExecutionError(e.to_string()))?;
let columns = xlsx.get_column_names(worksheet).map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(e.to_string()),
data: None,
})?;
Ok(vec![Content::text(format!("{:#?}", columns))])
}
"get_range" => {
let range = params
.get("range")
.and_then(|v| v.as_str())
.ok_or_else(|| {
ToolError::InvalidParameters("Missing 'range' parameter".into())
.ok_or_else(|| ErrorData {
code: ErrorCode::INVALID_PARAMS,
message: Cow::from("Missing 'range' parameter"),
data: None,
})?;
let xlsx = xlsx_tool::XlsxTool::new(path)
.map_err(|e| ToolError::ExecutionError(e.to_string()))?;
let xlsx = xlsx_tool::XlsxTool::new(path).map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(e.to_string()),
data: None,
})?;
let worksheet = if let Some(name) = params.get("worksheet").and_then(|v| v.as_str())
{
xlsx.get_worksheet_by_name(name)
.map_err(|e| ToolError::ExecutionError(e.to_string()))?
xlsx.get_worksheet_by_name(name).map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(e.to_string()),
data: None,
})?
} else {
xlsx.get_worksheet_by_index(0)
.map_err(|e| ToolError::ExecutionError(e.to_string()))?
xlsx.get_worksheet_by_index(0).map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(e.to_string()),
data: None,
})?
};
let range_data = xlsx
.get_range(worksheet, range)
.map_err(|e| ToolError::ExecutionError(e.to_string()))?;
let range_data = xlsx.get_range(worksheet, range).map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(e.to_string()),
data: None,
})?;
Ok(vec![Content::text(format!("{:#?}", range_data))])
}
"find_text" => {
let search_text = params
.get("search_text")
.and_then(|v| v.as_str())
.ok_or_else(|| {
ToolError::InvalidParameters("Missing 'search_text' parameter".into())
.ok_or_else(|| ErrorData {
code: ErrorCode::INVALID_PARAMS,
message: Cow::from("Missing 'search_text' parameter"),
data: None,
})?;
let case_sensitive = params
@@ -897,19 +1000,32 @@ impl ComputerControllerRouter {
.and_then(|v| v.as_bool())
.unwrap_or(false);
let xlsx = xlsx_tool::XlsxTool::new(path)
.map_err(|e| ToolError::ExecutionError(e.to_string()))?;
let xlsx = xlsx_tool::XlsxTool::new(path).map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(e.to_string()),
data: None,
})?;
let worksheet = if let Some(name) = params.get("worksheet").and_then(|v| v.as_str())
{
xlsx.get_worksheet_by_name(name)
.map_err(|e| ToolError::ExecutionError(e.to_string()))?
xlsx.get_worksheet_by_name(name).map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(e.to_string()),
data: None,
})?
} else {
xlsx.get_worksheet_by_index(0)
.map_err(|e| ToolError::ExecutionError(e.to_string()))?
xlsx.get_worksheet_by_index(0).map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(e.to_string()),
data: None,
})?
};
let matches = xlsx
.find_in_worksheet(worksheet, search_text, case_sensitive)
.map_err(|e| ToolError::ExecutionError(e.to_string()))?;
.map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(e.to_string()),
data: None,
})?;
Ok(vec![Content::text(format!(
"Found matches at: {:#?}",
matches
@@ -925,66 +1041,114 @@ impl ComputerControllerRouter {
.and_then(|v| v.as_str())
.unwrap_or("Sheet1");
let mut xlsx = xlsx_tool::XlsxTool::new(path)
.map_err(|e| ToolError::ExecutionError(e.to_string()))?;
let mut xlsx = xlsx_tool::XlsxTool::new(path).map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(e.to_string()),
data: None,
})?;
xlsx.update_cell(worksheet_name, row as u32, col as u32, value)
.map_err(|e| ToolError::ExecutionError(e.to_string()))?;
xlsx.save(path)
.map_err(|e| ToolError::ExecutionError(e.to_string()))?;
.map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(e.to_string()),
data: None,
})?;
xlsx.save(path).map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(e.to_string()),
data: None,
})?;
Ok(vec![Content::text(format!(
"Updated cell ({}, {}) to '{}' in worksheet '{}'",
row, col, value, worksheet_name
))])
}
"save" => {
let xlsx = xlsx_tool::XlsxTool::new(path)
.map_err(|e| ToolError::ExecutionError(e.to_string()))?;
xlsx.save(path)
.map_err(|e| ToolError::ExecutionError(e.to_string()))?;
let xlsx = xlsx_tool::XlsxTool::new(path).map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(e.to_string()),
data: None,
})?;
xlsx.save(path).map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(e.to_string()),
data: None,
})?;
Ok(vec![Content::text("File saved successfully.")])
}
"get_cell" => {
let row = params.get("row").and_then(|v| v.as_u64()).ok_or_else(|| {
ToolError::InvalidParameters("Missing 'row' parameter".into())
})?;
let row = params
.get("row")
.and_then(|v| v.as_u64())
.ok_or_else(|| ErrorData {
code: ErrorCode::INVALID_PARAMS,
message: Cow::from("Missing 'row' parameter"),
data: None,
})?;
let col = params.get("col").and_then(|v| v.as_u64()).ok_or_else(|| {
ToolError::InvalidParameters("Missing 'col' parameter".into())
})?;
let col = params
.get("col")
.and_then(|v| v.as_u64())
.ok_or_else(|| ErrorData {
code: ErrorCode::INVALID_PARAMS,
message: Cow::from("Missing 'col' parameter"),
data: None,
})?;
let xlsx = xlsx_tool::XlsxTool::new(path)
.map_err(|e| ToolError::ExecutionError(e.to_string()))?;
let xlsx = xlsx_tool::XlsxTool::new(path).map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(e.to_string()),
data: None,
})?;
let worksheet = if let Some(name) = params.get("worksheet").and_then(|v| v.as_str())
{
xlsx.get_worksheet_by_name(name)
.map_err(|e| ToolError::ExecutionError(e.to_string()))?
xlsx.get_worksheet_by_name(name).map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(e.to_string()),
data: None,
})?
} else {
xlsx.get_worksheet_by_index(0)
.map_err(|e| ToolError::ExecutionError(e.to_string()))?
xlsx.get_worksheet_by_index(0).map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(e.to_string()),
data: None,
})?
};
let cell_value = xlsx
.get_cell_value(worksheet, row as u32, col as u32)
.map_err(|e| ToolError::ExecutionError(e.to_string()))?;
.map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(e.to_string()),
data: None,
})?;
Ok(vec![Content::text(format!("{:#?}", cell_value))])
}
_ => Err(ToolError::InvalidParameters(format!(
"Invalid operation: {}",
operation
))),
_ => Err(ErrorData {
code: ErrorCode::INVALID_PARAMS,
message: Cow::from(format!("Invalid operation: {}", operation)),
data: None,
}),
}
}
// Implement cache tool functionality
async fn docx_tool(&self, params: Value) -> Result<Vec<Content>, ToolError> {
async fn docx_tool(&self, params: Value) -> Result<Vec<Content>, ErrorData> {
let path = params
.get("path")
.and_then(|v| v.as_str())
.ok_or_else(|| ToolError::InvalidParameters("Missing 'path' parameter".into()))?;
.ok_or_else(|| ErrorData {
code: ErrorCode::INVALID_PARAMS,
message: Cow::from("Missing 'path' parameter"),
data: None,
})?;
let operation = params
.get("operation")
.and_then(|v| v.as_str())
.ok_or_else(|| ToolError::InvalidParameters("Missing 'operation' parameter".into()))?;
.ok_or_else(|| ErrorData {
code: ErrorCode::INVALID_PARAMS,
message: Cow::from("Missing 'operation' parameter"),
data: None,
})?;
crate::computercontroller::docx_tool::docx_tool(
path,
@@ -995,34 +1159,50 @@ impl ComputerControllerRouter {
.await
}
async fn pdf_tool(&self, params: Value) -> Result<Vec<Content>, ToolError> {
async fn pdf_tool(&self, params: Value) -> Result<Vec<Content>, ErrorData> {
let path = params
.get("path")
.and_then(|v| v.as_str())
.ok_or_else(|| ToolError::InvalidParameters("Missing 'path' parameter".into()))?;
.ok_or_else(|| ErrorData {
code: ErrorCode::INVALID_PARAMS,
message: Cow::from("Missing 'path' parameter"),
data: None,
})?;
let operation = params
.get("operation")
.and_then(|v| v.as_str())
.ok_or_else(|| ToolError::InvalidParameters("Missing 'operation' parameter".into()))?;
.ok_or_else(|| ErrorData {
code: ErrorCode::INVALID_PARAMS,
message: Cow::from("Missing 'operation' parameter"),
data: None,
})?;
crate::computercontroller::pdf_tool::pdf_tool(path, operation, &self.cache_dir).await
}
async fn cache(&self, params: Value) -> Result<Vec<Content>, ToolError> {
async fn cache(&self, params: Value) -> Result<Vec<Content>, ErrorData> {
let command = params
.get("command")
.and_then(|v| v.as_str())
.ok_or_else(|| ToolError::InvalidParameters("Missing 'command' parameter".into()))?;
.ok_or_else(|| ErrorData {
code: ErrorCode::INVALID_PARAMS,
message: Cow::from("Missing 'command' parameter"),
data: None,
})?;
match command {
"list" => {
let mut files = Vec::new();
for entry in fs::read_dir(&self.cache_dir).map_err(|e| {
ToolError::ExecutionError(format!("Failed to read cache directory: {}", e))
for entry in fs::read_dir(&self.cache_dir).map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!("Failed to read cache directory: {}", e)),
data: None,
})? {
let entry = entry.map_err(|e| {
ToolError::ExecutionError(format!("Failed to read directory entry: {}", e))
let entry = entry.map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!("Failed to read directory entry: {}", e)),
data: None,
})?;
files.push(format!("{}", entry.path().display()));
}
@@ -1033,13 +1213,19 @@ impl ComputerControllerRouter {
))])
}
"view" => {
let path = params.get("path").and_then(|v| v.as_str()).ok_or_else(|| {
ToolError::InvalidParameters("Missing 'path' parameter for view".into())
})?;
let path = params.get("path").and_then(|v| v.as_str()).ok_or_else(||
ErrorData {
code: ErrorCode::INVALID_PARAMS,
message: Cow::from("Missing 'path' parameter for view"),
data: None,
})?;
let content = fs::read_to_string(path).map_err(|e| {
ToolError::ExecutionError(format!("Failed to read file: {}", e))
})?;
let content = fs::read_to_string(path).map_err(|e|
ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!("Failed to read file: {}", e)),
data: None,
})?;
Ok(vec![Content::text(format!(
"Content of {}:\n\n{}",
@@ -1047,13 +1233,19 @@ impl ComputerControllerRouter {
))])
}
"delete" => {
let path = params.get("path").and_then(|v| v.as_str()).ok_or_else(|| {
ToolError::InvalidParameters("Missing 'path' parameter for delete".into())
})?;
let path = params.get("path").and_then(|v| v.as_str()).ok_or_else(||
ErrorData {
code: ErrorCode::INVALID_PARAMS,
message: Cow::from("Missing 'path' parameter for delete"),
data: None,
})?;
fs::remove_file(path).map_err(|e| {
ToolError::ExecutionError(format!("Failed to delete file: {}", e))
})?;
fs::remove_file(path).map_err(|e|
ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!("Failed to delete file: {}", e)),
data: None,
})?;
// Remove from active resources if present
if let Ok(url) = Url::from_file_path(path) {
@@ -1066,22 +1258,31 @@ impl ComputerControllerRouter {
Ok(vec![Content::text(format!("Deleted file: {}", path))])
}
"clear" => {
fs::remove_dir_all(&self.cache_dir).map_err(|e| {
ToolError::ExecutionError(format!("Failed to clear cache directory: {}", e))
})?;
fs::create_dir_all(&self.cache_dir).map_err(|e| {
ToolError::ExecutionError(format!("Failed to recreate cache directory: {}", e))
})?;
fs::remove_dir_all(&self.cache_dir).map_err(|e|
ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!("Failed to clear cache directory: {}", e)),
data: None,
})?;
fs::create_dir_all(&self.cache_dir).map_err(|e|
ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!("Failed to recreate cache directory: {}", e)),
data: None,
})?;
// Clear active resources
self.active_resources.lock().unwrap().clear();
Ok(vec![Content::text("Cache cleared successfully.")])
}
_ => Err(ToolError::InvalidParameters(format!(
_ => Err(ErrorData {
code: ErrorCode::INVALID_PARAMS,
message: Cow::from(format!(
"Invalid 'command' parameter: {}. Valid options are: 'list', 'view', 'delete', 'clear'",
command
)))
command)),
data: None,
}),
}
}
}
@@ -1111,7 +1312,7 @@ impl Router for ComputerControllerRouter {
tool_name: &str,
arguments: Value,
_notifier: mpsc::Sender<JsonRpcMessage>,
) -> Pin<Box<dyn Future<Output = Result<Vec<Content>, ToolError>> + Send + 'static>> {
) -> Pin<Box<dyn Future<Output = Result<Vec<Content>, ErrorData>> + Send + 'static>> {
let this = self.clone();
let tool_name = tool_name.to_string();
Box::pin(async move {
@@ -1123,7 +1324,11 @@ impl Router for ComputerControllerRouter {
"pdf_tool" => this.pdf_tool(arguments).await,
"docx_tool" => this.docx_tool(arguments).await,
"xlsx_tool" => this.xlsx_tool(arguments).await,
_ => Err(ToolError::NotFound(format!("Tool {} not found", tool_name))),
_ => Err(ErrorData {
code: ErrorCode::INVALID_REQUEST,
message: Cow::from(format!("Tool {} not found", tool_name)),
data: None,
}),
}
})
}
@@ -1,16 +1,20 @@
use lopdf::{content::Content as PdfContent, Document, Object};
use mcp_core::ToolError;
use rmcp::model::Content;
use rmcp::model::{Content, ErrorCode, ErrorData};
use std::{fs, path::Path};
pub async fn pdf_tool(
path: &str,
operation: &str,
cache_dir: &Path,
) -> Result<Vec<Content>, ToolError> {
) -> Result<Vec<Content>, ErrorData> {
// Open and parse the PDF file
let doc = Document::load(path)
.map_err(|e| ToolError::ExecutionError(format!("Failed to open PDF file: {}", e)))?;
let doc = Document::load(path).map_err(|e| {
ErrorData::new(
ErrorCode::INTERNAL_ERROR,
format!("Failed to open PDF file: {}", e),
None,
)
})?;
let result = match operation {
"extract_text" => {
@@ -115,7 +119,11 @@ pub async fn pdf_tool(
"extract_images" => {
let cache_dir = cache_dir.join("pdf_images");
fs::create_dir_all(&cache_dir).map_err(|e| {
ToolError::ExecutionError(format!("Failed to create image cache directory: {}", e))
ErrorData::new(
ErrorCode::INTERNAL_ERROR,
format!("Failed to create image cache directory: {}", e),
None,
)
})?;
let mut images = Vec::new();
@@ -167,14 +175,19 @@ pub async fn pdf_tool(
// Process each page
for (page_num, page_id) in doc.get_pages() {
let page = doc.get_object(page_id).map_err(|e| {
ToolError::ExecutionError(format!("Failed to get page {}: {}", page_num, e))
ErrorData::new(
ErrorCode::INTERNAL_ERROR,
format!("Failed to get page {}: {}", page_num, e),
None,
)
})?;
let page_dict = page.as_dict().map_err(|e| {
ToolError::ExecutionError(format!(
"Failed to get page dict {}: {}",
page_num, e
))
ErrorData::new(
ErrorCode::INTERNAL_ERROR,
format!("Failed to get page dict {}: {}", page_num, e),
None,
)
})?;
// Get page resources - handle both direct dict and reference
@@ -184,27 +197,32 @@ pub async fn pdf_tool(
Object::Reference(id) => doc
.get_object(*id)
.map_err(|e| {
ToolError::ExecutionError(format!(
"Failed to get resource reference: {}",
e
))
ErrorData::new(
ErrorCode::RESOURCE_NOT_FOUND,
format!("Failed to get resource reference: {}", e),
None,
)
})
.and_then(|obj| {
obj.as_dict().map_err(|e| {
ToolError::ExecutionError(format!(
"Resource reference is not a dictionary: {}",
e
))
ErrorData::new(
ErrorCode::RESOURCE_NOT_FOUND,
format!("Resource reference is not a dictionary: {}", e),
None,
)
})
}),
_ => Err(ToolError::ExecutionError(
_ => Err(ErrorData::new(
ErrorCode::INTERNAL_ERROR,
"Resources is neither dictionary nor reference".to_string(),
None,
)),
},
Err(e) => Err(ToolError::ExecutionError(format!(
"Failed to get Resources: {}",
e
))),
Err(e) => Err(ErrorData::new(
ErrorCode::INTERNAL_ERROR,
format!("Failed to get Resources: {}", e),
None,
)),
}?;
// Look for XObject dictionary - handle both direct dict and reference
@@ -214,37 +232,50 @@ pub async fn pdf_tool(
Object::Reference(id) => doc
.get_object(*id)
.map_err(|e| {
ToolError::ExecutionError(format!(
"Failed to get XObject reference: {}",
e
))
ErrorData::new(
ErrorCode::INTERNAL_ERROR,
format!("Failed to get XObject reference: {}", e),
None,
)
})
.and_then(|obj| {
obj.as_dict().map_err(|e| {
ToolError::ExecutionError(format!(
"XObject reference is not a dictionary: {}",
e
))
ErrorData::new(
ErrorCode::INTERNAL_ERROR,
format!("XObject reference is not a dictionary: {}", e),
None,
)
})
}),
_ => Err(ToolError::ExecutionError(
_ => Err(ErrorData::new(
ErrorCode::INTERNAL_ERROR,
"XObject is neither dictionary nor reference".to_string(),
None,
)),
},
Err(e) => Err(ToolError::ExecutionError(format!(
"Failed to get XObject: {}",
e
))),
Err(e) => Err(ErrorData::new(
ErrorCode::INTERNAL_ERROR,
format!("Failed to get XObject: {}", e),
None,
)),
};
if let Ok(xobjects) = xobjects {
for (name, xobject) in xobjects.iter() {
let xobject_id = xobject.as_reference().map_err(|_| {
ToolError::ExecutionError("Failed to get XObject reference".to_string())
ErrorData::new(
ErrorCode::INTERNAL_ERROR,
"Failed to get XObject reference".to_string(),
None,
)
})?;
let xobject = doc.get_object(xobject_id).map_err(|e| {
ToolError::ExecutionError(format!("Failed to get XObject: {}", e))
ErrorData::new(
ErrorCode::INTERNAL_ERROR,
format!("Failed to get XObject: {}", e),
None,
)
})?;
if let Ok(stream) = xobject.as_stream() {
@@ -283,10 +314,11 @@ pub async fn pdf_tool(
));
fs::write(&image_path, &data).map_err(|e| {
ToolError::ExecutionError(format!(
"Failed to write image: {}",
e
))
ErrorData::new(
ErrorCode::INTERNAL_ERROR,
format!("Failed to write image: {}", e),
None,
)
})?;
images.push(format!(
@@ -313,10 +345,14 @@ pub async fn pdf_tool(
}
_ => {
return Err(ToolError::InvalidParameters(format!(
"Invalid operation: {}. Valid operations are: 'extract_text', 'extract_images'",
operation
)))
return Err(ErrorData::new(
ErrorCode::INVALID_PARAMS,
format!(
"Invalid operation: {}. Valid operations are: 'extract_text', 'extract_images'",
operation
),
None,
))
}
};
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+10 -4
View File
@@ -2,13 +2,15 @@ use async_trait::async_trait;
use etcetera::{choose_app_strategy, AppStrategy};
use indoc::formatdoc;
use mcp_core::{
handler::{PromptError, ResourceError, ToolError},
handler::{PromptError, ResourceError},
protocol::ServerCapabilities,
tool::ToolCall,
};
use mcp_server::router::CapabilitiesBuilder;
use mcp_server::Router;
use rmcp::model::{Content, JsonRpcMessage, Prompt, Resource, Tool, ToolAnnotations};
use rmcp::model::{
Content, ErrorCode, ErrorData, JsonRpcMessage, Prompt, Resource, Tool, ToolAnnotations,
};
use rmcp::object;
use serde_json::Value;
use std::{
@@ -523,7 +525,7 @@ impl Router for MemoryRouter {
tool_name: &str,
arguments: Value,
_notifier: mpsc::Sender<JsonRpcMessage>,
) -> Pin<Box<dyn Future<Output = Result<Vec<Content>, ToolError>> + Send + 'static>> {
) -> Pin<Box<dyn Future<Output = Result<Vec<Content>, ErrorData>> + Send + 'static>> {
let this = self.clone();
let tool_name = tool_name.to_string();
@@ -534,7 +536,11 @@ impl Router for MemoryRouter {
};
match this.execute_tool_call(tool_call).await {
Ok(result) => Ok(vec![Content::text(result)]),
Err(err) => Err(ToolError::ExecutionError(err.to_string())),
Err(err) => Err(ErrorData::new(
ErrorCode::INTERNAL_ERROR,
err.to_string(),
None,
)),
}
})
}
+21 -12
View File
@@ -2,12 +2,14 @@ use anyhow::Result;
use include_dir::{include_dir, Dir};
use indoc::formatdoc;
use mcp_core::{
handler::{PromptError, ResourceError, ToolError},
handler::{PromptError, ResourceError},
protocol::ServerCapabilities,
};
use mcp_server::router::CapabilitiesBuilder;
use mcp_server::Router;
use rmcp::model::{Content, JsonRpcMessage, Prompt, Resource, Role, Tool, ToolAnnotations};
use rmcp::model::{
Content, ErrorCode, ErrorData, JsonRpcMessage, Prompt, Resource, Role, Tool, ToolAnnotations,
};
use rmcp::object;
use serde_json::Value;
use std::{future::Future, pin::Pin};
@@ -92,14 +94,13 @@ impl TutorialRouter {
tutorials
}
async fn load_tutorial(&self, name: &str) -> Result<String, ToolError> {
async fn load_tutorial(&self, name: &str) -> Result<String, ErrorData> {
let file_name = format!("{}.md", name);
let file = TUTORIALS_DIR
.get_file(&file_name)
.ok_or(ToolError::ExecutionError(format!(
"Could not locate tutorial '{}'",
name
)))?;
let file = TUTORIALS_DIR.get_file(&file_name).ok_or(ErrorData::new(
ErrorCode::INTERNAL_ERROR,
format!("Could not locate tutorial '{}'", name),
None,
))?;
Ok(String::from_utf8_lossy(file.contents()).into_owned())
}
}
@@ -126,7 +127,7 @@ impl Router for TutorialRouter {
tool_name: &str,
arguments: Value,
_notifier: mpsc::Sender<JsonRpcMessage>,
) -> Pin<Box<dyn Future<Output = Result<Vec<Content>, ToolError>> + Send + 'static>> {
) -> Pin<Box<dyn Future<Output = Result<Vec<Content>, ErrorData>> + Send + 'static>> {
let this = self.clone();
let tool_name = tool_name.to_string();
@@ -137,7 +138,11 @@ impl Router for TutorialRouter {
.get("name")
.and_then(|v| v.as_str())
.ok_or_else(|| {
ToolError::InvalidParameters("Missing 'name' parameter".to_string())
ErrorData::new(
ErrorCode::INVALID_PARAMS,
"Missing 'name' parameter".to_string(),
None,
)
})?;
let content = this.load_tutorial(name).await?;
@@ -145,7 +150,11 @@ impl Router for TutorialRouter {
Content::text(content).with_audience(vec![Role::Assistant])
])
}
_ => Err(ToolError::NotFound(format!("Tool {} not found", tool_name))),
_ => Err(ErrorData::new(
ErrorCode::RESOURCE_NOT_FOUND,
format!("Tool {} not found", tool_name),
None,
)),
}
})
}
+47 -25
View File
@@ -44,9 +44,11 @@ use crate::scheduler_trait::SchedulerTrait;
use crate::session;
use crate::tool_monitor::{ToolCall, ToolMonitor};
use crate::utils::is_token_cancelled;
use mcp_core::{ToolError, ToolResult};
use mcp_core::ToolResult;
use regex::Regex;
use rmcp::model::{Content, GetPromptResult, Prompt, ServerNotification, Tool};
use rmcp::model::{
Content, ErrorCode, ErrorData, GetPromptResult, Prompt, ServerNotification, Tool,
};
use serde_json::Value;
use tokio::sync::{mpsc, Mutex, RwLock};
use tokio_util::sync::CancellationToken;
@@ -376,7 +378,7 @@ impl Agent {
tool_call: mcp_core::tool::ToolCall,
request_id: String,
cancellation_token: Option<CancellationToken>,
) -> (String, Result<ToolCallResult, ToolError>) {
) -> (String, Result<ToolCallResult, ErrorData>) {
// Check if this tool call should be allowed based on repetition monitoring
if let Some(monitor) = self.tool_monitor.lock().await.as_mut() {
let tool_call_info = ToolCall::new(tool_call.name.clone(), tool_call.arguments.clone());
@@ -384,8 +386,10 @@ impl Agent {
if !monitor.check_tool_call(tool_call_info) {
return (
request_id,
Err(ToolError::ExecutionError(
Err(ErrorData::new(
ErrorCode::INTERNAL_ERROR,
"Tool call rejected: exceeded maximum allowed repetitions".to_string(),
None,
)),
);
}
@@ -425,8 +429,10 @@ impl Agent {
} else {
(
request_id,
Err(ToolError::ExecutionError(
Err(ErrorData::new(
ErrorCode::INTERNAL_ERROR,
"Final output tool not defined".to_string(),
None,
)),
)
};
@@ -478,8 +484,10 @@ impl Agent {
ToolCallResult::from(extension_manager.search_available_extensions().await)
} else if self.is_frontend_tool(&tool_call.name).await {
// For frontend tools, return an error indicating we need frontend execution
ToolCallResult::from(Err(ToolError::ExecutionError(
ToolCallResult::from(Err(ErrorData::new(
ErrorCode::INTERNAL_ERROR,
"Frontend tool execution required".to_string(),
None,
)))
} else if tool_call.name == TODO_READ_TOOL_NAME {
// Handle task planner read tool
@@ -505,11 +513,13 @@ impl Agent {
if max_chars > 0 && char_count > max_chars {
return (
request_id,
Ok(ToolCallResult::from(Err(ToolError::ExecutionError(
Ok(ToolCallResult::from(Err(ErrorData::new(
ErrorCode::INTERNAL_ERROR,
format!(
"Todo list too large: {} chars (max: {})",
char_count, max_chars
),
None,
)))),
);
}
@@ -537,7 +547,11 @@ impl Agent {
.dispatch_tool_call(tool_call.clone(), cancellation_token.unwrap_or_default())
.await;
result.unwrap_or_else(|e| {
ToolCallResult::from(Err(ToolError::ExecutionError(e.to_string())))
ToolCallResult::from(Err(ErrorData::new(
ErrorCode::INTERNAL_ERROR,
e.to_string(),
None,
)))
})
};
@@ -554,12 +568,13 @@ impl Agent {
)
}
#[allow(clippy::too_many_lines)]
pub(super) async fn manage_extensions(
&self,
action: String,
extension_name: String,
request_id: String,
) -> (String, Result<Vec<Content>, ToolError>) {
) -> (String, Result<Vec<Content>, ErrorData>) {
let selector = self.tool_route_manager.get_router_tool_selector().await;
if ToolRouterIndexManager::is_tool_router_enabled(&selector) {
if let Some(selector) = selector {
@@ -576,10 +591,11 @@ impl Agent {
{
return (
request_id,
Err(ToolError::ExecutionError(format!(
"Failed to update vector index: {}",
e
))),
Err(ErrorData::new(
ErrorCode::INTERNAL_ERROR,
format!("Failed to update vector index: {}", e),
None,
)),
);
}
}
@@ -595,7 +611,7 @@ impl Agent {
extension_name
))]
})
.map_err(|e| ToolError::ExecutionError(e.to_string()));
.map_err(|e| ErrorData::new(ErrorCode::INTERNAL_ERROR, e.to_string(), None));
return (request_id, result);
}
@@ -604,19 +620,24 @@ impl Agent {
Ok(None) => {
return (
request_id,
Err(ToolError::ExecutionError(format!(
Err(ErrorData::new(
ErrorCode::RESOURCE_NOT_FOUND,
format!(
"Extension '{}' not found. Please check the extension name and try again.",
extension_name
))),
),
None,
)),
)
}
Err(e) => {
return (
request_id,
Err(ToolError::ExecutionError(format!(
"Failed to get extension config: {}",
e
))),
Err(ErrorData::new(
ErrorCode::INTERNAL_ERROR,
format!("Failed to get extension config: {}", e),
None,
)),
)
}
};
@@ -629,7 +650,7 @@ impl Agent {
extension_name
))]
})
.map_err(|e| ToolError::ExecutionError(e.to_string()));
.map_err(|e| ErrorData::new(ErrorCode::INTERNAL_ERROR, e.to_string(), None));
drop(extension_manager);
// Update vector index if operation was successful and vector routing is enabled
@@ -650,10 +671,11 @@ impl Agent {
{
return (
request_id,
Err(ToolError::ExecutionError(format!(
"Failed to update vector index: {}",
e
))),
Err(ErrorData::new(
ErrorCode::INTERNAL_ERROR,
format!("Failed to update vector index: {}", e),
None,
)),
);
}
}
+59 -35
View File
@@ -4,7 +4,7 @@ use chrono::{DateTime, Utc};
use futures::stream::{FuturesUnordered, StreamExt};
use futures::{future, FutureExt};
use mcp_core::handler::require_str_parameter;
use mcp_core::{ToolCall, ToolError};
use mcp_core::ToolCall;
use rmcp::service::ClientInitializeError;
use rmcp::transport::streamable_http_client::StreamableHttpClientTransportConfig;
use rmcp::transport::{
@@ -30,7 +30,7 @@ use crate::config::{Config, ExtensionConfigManager};
use crate::oauth::oauth_flow;
use crate::prompt_template;
use mcp_client::client::{McpClient, McpClientTrait};
use rmcp::model::{Content, GetPromptResult, Prompt, ResourceContents, Tool};
use rmcp::model::{Content, ErrorCode, ErrorData, GetPromptResult, Prompt, ResourceContents, Tool};
use rmcp::transport::auth::AuthClient;
use serde_json::Value;
@@ -546,7 +546,7 @@ impl ExtensionManager {
&self,
params: Value,
cancellation_token: CancellationToken,
) -> Result<Vec<Content>, ToolError> {
) -> Result<Vec<Content>, ErrorData> {
let uri = require_str_parameter(&params, "uri")?;
let extension_name = params.get("extension_name").and_then(|v| v.as_str());
@@ -588,7 +588,11 @@ impl ExtensionManager {
uri, available_extensions
);
Err(ToolError::InvalidParameters(error_msg))
Err(ErrorData::new(
ErrorCode::RESOURCE_NOT_FOUND,
error_msg,
None,
))
}
async fn read_resource_from_extension(
@@ -596,7 +600,7 @@ impl ExtensionManager {
uri: &str,
extension_name: &str,
cancellation_token: CancellationToken,
) -> Result<Vec<Content>, ToolError> {
) -> Result<Vec<Content>, ErrorData> {
let available_extensions = self
.clients
.keys()
@@ -608,17 +612,22 @@ impl ExtensionManager {
extension_name, available_extensions
);
let client = self
.clients
.get(extension_name)
.ok_or(ToolError::InvalidParameters(error_msg))?;
let client = self.clients.get(extension_name).ok_or(ErrorData::new(
ErrorCode::INVALID_PARAMS,
error_msg,
None,
))?;
let client_guard = client.lock().await;
let read_result = client_guard
.read_resource(uri, cancellation_token)
.await
.map_err(|_| {
ToolError::ExecutionError(format!("Could not read resource with uri: {}", uri))
ErrorData::new(
ErrorCode::INTERNAL_ERROR,
format!("Could not read resource with uri: {}", uri),
None,
)
})?;
let mut result = Vec::new();
@@ -637,9 +646,13 @@ impl ExtensionManager {
&self,
extension_name: &str,
cancellation_token: CancellationToken,
) -> Result<Vec<Content>, ToolError> {
) -> Result<Vec<Content>, ErrorData> {
let client = self.clients.get(extension_name).ok_or_else(|| {
ToolError::InvalidParameters(format!("Extension {} is not valid", extension_name))
ErrorData::new(
ErrorCode::INVALID_PARAMS,
format!("Extension {} is not valid", extension_name),
None,
)
})?;
let client_guard = client.lock().await;
@@ -647,10 +660,11 @@ impl ExtensionManager {
.list_resources(None, cancellation_token)
.await
.map_err(|e| {
ToolError::ExecutionError(format!(
"Unable to list resources for {}, {:?}",
extension_name, e
))
ErrorData::new(
ErrorCode::INTERNAL_ERROR,
format!("Unable to list resources for {}, {:?}", extension_name, e),
None,
)
})
.map(|lr| {
let resource_list = lr
@@ -668,7 +682,7 @@ impl ExtensionManager {
&self,
params: Value,
cancellation_token: CancellationToken,
) -> Result<Vec<Content>, ToolError> {
) -> Result<Vec<Content>, ErrorData> {
let extension = params.get("extension").and_then(|v| v.as_str());
match extension {
@@ -727,16 +741,18 @@ impl ExtensionManager {
cancellation_token: CancellationToken,
) -> Result<ToolCallResult> {
// Dispatch tool call based on the prefix naming convention
let (client_name, client) = self
.get_client_for_tool(&tool_call.name)
.ok_or_else(|| ToolError::NotFound(tool_call.name.clone()))?;
let (client_name, client) = self.get_client_for_tool(&tool_call.name).ok_or_else(|| {
ErrorData::new(ErrorCode::RESOURCE_NOT_FOUND, tool_call.name.clone(), None)
})?;
// rsplit returns the iterator in reverse, tool_name is then at 0
let tool_name = tool_call
.name
.strip_prefix(client_name)
.and_then(|s| s.strip_prefix("__"))
.ok_or_else(|| ToolError::NotFound(tool_call.name.clone()))?
.ok_or_else(|| {
ErrorData::new(ErrorCode::RESOURCE_NOT_FOUND, tool_call.name.clone(), None)
})?
.to_string();
let arguments = tool_call.arguments.clone();
@@ -749,7 +765,7 @@ impl ExtensionManager {
.call_tool(&tool_name, arguments, cancellation_token)
.await
.map(|call| call.content.unwrap_or_default())
.map_err(|e| ToolError::ExecutionError(e.to_string()))
.map_err(|e| ErrorData::new(ErrorCode::INTERNAL_ERROR, e.to_string(), None))
};
Ok(ToolCallResult {
@@ -762,9 +778,13 @@ impl ExtensionManager {
&self,
extension_name: &str,
cancellation_token: CancellationToken,
) -> Result<Vec<Prompt>, ToolError> {
) -> Result<Vec<Prompt>, ErrorData> {
let client = self.clients.get(extension_name).ok_or_else(|| {
ToolError::InvalidParameters(format!("Extension {} is not valid", extension_name))
ErrorData::new(
ErrorCode::INVALID_PARAMS,
format!("Extension {} is not valid", extension_name),
None,
)
})?;
let client_guard = client.lock().await;
@@ -772,10 +792,11 @@ impl ExtensionManager {
.list_prompts(None, cancellation_token)
.await
.map_err(|e| {
ToolError::ExecutionError(format!(
"Unable to list prompts for {}, {:?}",
extension_name, e
))
ErrorData::new(
ErrorCode::INTERNAL_ERROR,
format!("Unable to list prompts for {}, {:?}", extension_name, e),
None,
)
})
.map(|lp| lp.prompts)
}
@@ -783,7 +804,7 @@ impl ExtensionManager {
pub async fn list_prompts(
&self,
cancellation_token: CancellationToken,
) -> Result<HashMap<String, Vec<Prompt>>, ToolError> {
) -> Result<HashMap<String, Vec<Prompt>>, ErrorData> {
let mut futures = FuturesUnordered::new();
for extension_name in self.clients.keys() {
@@ -846,7 +867,7 @@ impl ExtensionManager {
.map_err(|e| anyhow::anyhow!("Failed to get prompt: {}", e))
}
pub async fn search_available_extensions(&self) -> Result<Vec<Content>, ToolError> {
pub async fn search_available_extensions(&self) -> Result<Vec<Content>, ErrorData> {
let mut output_parts = vec![];
// First get disabled extensions from current config
@@ -1140,8 +1161,11 @@ mod tests {
.result
.await;
assert!(matches!(
result.err().unwrap(),
ToolError::ExecutionError(_)
result,
Err(ErrorData {
code: ErrorCode::INTERNAL_ERROR,
..
})
));
// this should error out, specifically with an ToolError::NotFound
@@ -1155,10 +1179,10 @@ mod tests {
.dispatch_tool_call(invalid_tool_call, CancellationToken::default())
.await;
if let Err(err) = result {
let tool_err = err.downcast_ref::<ToolError>().expect("Expected ToolError");
assert!(matches!(tool_err, ToolError::NotFound(_)));
let tool_err = err.downcast_ref::<ErrorData>().expect("Expected ErrorData");
assert_eq!(tool_err.code, ErrorCode::RESOURCE_NOT_FOUND);
} else {
panic!("Expected ToolError::NotFound");
panic!("Expected ErrorData with ErrorCode::RESOURCE_NOT_FOUND");
}
}
}
+13 -7
View File
@@ -1,9 +1,10 @@
use crate::agents::tool_execution::ToolCallResult;
use crate::recipe::Response;
use indoc::formatdoc;
use mcp_core::{ToolCall, ToolError};
use rmcp::model::{Content, Tool, ToolAnnotations};
use mcp_core::ToolCall;
use rmcp::model::{Content, ErrorCode, ErrorData, Tool, ToolAnnotations};
use serde_json::Value;
use std::borrow::Cow;
pub const FINAL_OUTPUT_TOOL_NAME: &str = "recipe__final_output";
pub const FINAL_OUTPUT_CONTINUATION_MESSAGE: &str =
@@ -127,13 +128,18 @@ impl FinalOutputTool {
"Final output successfully collected.".to_string(),
)]))
}
Err(error) => ToolCallResult::from(Err(ToolError::InvalidParameters(error))),
Err(error) => ToolCallResult::from(Err(ErrorData {
code: ErrorCode::INVALID_PARAMS,
message: Cow::from(error),
data: None,
})),
}
}
_ => ToolCallResult::from(Err(ToolError::NotFound(format!(
"Unknown tool: {}",
tool_call.name
)))),
_ => ToolCallResult::from(Err(ErrorData {
code: ErrorCode::INVALID_REQUEST,
message: Cow::from(format!("Unknown tool: {}", tool_call.name)),
data: None,
})),
}
}
@@ -1,6 +1,5 @@
use chrono::Utc;
use mcp_core::ToolError;
use rmcp::model::Content;
use rmcp::model::{Content, ErrorData};
use std::fs::File;
use std::io::Write;
@@ -8,8 +7,8 @@ const LARGE_TEXT_THRESHOLD: usize = 200_000;
/// Process tool response and handle large text content
pub fn process_tool_response(
response: Result<Vec<Content>, ToolError>,
) -> Result<Vec<Content>, ToolError> {
response: Result<Vec<Content>, ErrorData>,
) -> Result<Vec<Content>, ErrorData> {
match response {
Ok(contents) => {
let mut processed_contents = Vec::new();
@@ -79,8 +78,8 @@ fn write_large_text_to_file(content: &str) -> Result<String, std::io::Error> {
#[cfg(test)]
mod tests {
use super::*;
use mcp_core::ToolError;
use rmcp::model::Content;
use rmcp::model::{Content, ErrorCode, ErrorData};
use std::borrow::Cow;
use std::fs;
use std::path::Path;
@@ -213,8 +212,12 @@ mod tests {
#[test]
fn test_error_response_passes_through() {
// Create an error response
let error = ToolError::ExecutionError("Test error".to_string());
let response: Result<Vec<Content>, ToolError> = Err(error);
let error = ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from("Test error"),
data: None,
};
let response: Result<Vec<Content>, ErrorData> = Err(error);
// Process the response
let processed = process_tool_response(response);
@@ -222,8 +225,9 @@ mod tests {
// Verify the error is passed through unchanged
assert!(processed.is_err());
match processed {
Err(ToolError::ExecutionError(msg)) => {
assert_eq!(msg, "Test error");
Err(err) => {
assert_eq!(err.code, ErrorCode::INTERNAL_ERROR);
assert_eq!(err.message, "Test error");
}
_ => panic!("Expected execution error"),
}
@@ -5,10 +5,10 @@
use crate::agents::subagent_execution_tool::tasks_manager::TasksManager;
use crate::agents::subagent_execution_tool::{lib::ExecutionMode, task_types::Task};
use crate::agents::tool_execution::ToolCallResult;
use mcp_core::ToolError;
use rmcp::model::{Content, Tool, ToolAnnotations};
use rmcp::model::{Content, ErrorCode, ErrorData, Tool, ToolAnnotations};
use rmcp::object;
use serde_json::{json, Value};
use std::borrow::Cow;
pub const DYNAMIC_TASK_TOOL_NAME_PREFIX: &str = "dynamic_task__create_task";
@@ -110,9 +110,11 @@ pub async fn create_dynamic_task(params: Value, tasks_manager: &TasksManager) ->
let task_params_array = extract_task_parameters(&params);
if task_params_array.is_empty() {
return ToolCallResult::from(Err(ToolError::ExecutionError(
"No task parameters provided".to_string(),
)));
return ToolCallResult::from(Err(ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from("No task parameters provided"),
data: None,
}));
}
let tasks = create_text_instruction_tasks_from_params(&task_params_array);
@@ -129,10 +131,11 @@ pub async fn create_dynamic_task(params: Value, tasks_manager: &TasksManager) ->
let tasks_json = match serde_json::to_string(&task_execution_payload) {
Ok(json) => json,
Err(e) => {
return ToolCallResult::from(Err(ToolError::ExecutionError(format!(
"Failed to serialize task list: {}",
e
))))
return ToolCallResult::from(Err(ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!("Failed to serialize task list: {}", e)),
data: None,
}))
}
};
tasks_manager.save_tasks(tasks.clone()).await;
+81 -43
View File
@@ -1,11 +1,11 @@
use mcp_core::ToolError;
use rmcp::model::Content;
use rmcp::model::Tool;
use rmcp::model::{Content, ErrorCode, ErrorData};
use anyhow::{Context, Result};
use async_trait::async_trait;
use serde::Serialize;
use serde_json::Value;
use std::borrow::Cow;
use std::collections::HashMap;
use std::collections::VecDeque;
use std::env;
@@ -32,11 +32,11 @@ pub enum RouterToolSelectionStrategy {
#[async_trait]
pub trait RouterToolSelector: Send + Sync {
async fn select_tools(&self, params: Value) -> Result<Vec<Content>, ToolError>;
async fn index_tools(&self, tools: &[Tool], extension_name: &str) -> Result<(), ToolError>;
async fn remove_tool(&self, tool_name: &str) -> Result<(), ToolError>;
async fn record_tool_call(&self, tool_name: &str) -> Result<(), ToolError>;
async fn get_recent_tool_calls(&self, limit: usize) -> Result<Vec<String>, ToolError>;
async fn select_tools(&self, params: Value) -> Result<Vec<Content>, ErrorData>;
async fn index_tools(&self, tools: &[Tool], extension_name: &str) -> Result<(), ErrorData>;
async fn remove_tool(&self, tool_name: &str) -> Result<(), ErrorData>;
async fn record_tool_call(&self, tool_name: &str) -> Result<(), ErrorData>;
async fn get_recent_tool_calls(&self, limit: usize) -> Result<Vec<String>, ErrorData>;
fn selector_type(&self) -> RouterToolSelectionStrategy;
}
@@ -80,11 +80,15 @@ impl VectorToolSelector {
#[async_trait]
impl RouterToolSelector for VectorToolSelector {
async fn select_tools(&self, params: Value) -> Result<Vec<Content>, ToolError> {
async fn select_tools(&self, params: Value) -> Result<Vec<Content>, ErrorData> {
let query = params
.get("query")
.and_then(|v| v.as_str())
.ok_or_else(|| ToolError::InvalidParameters("Missing 'query' parameter".to_string()))?;
.ok_or_else(|| ErrorData {
code: ErrorCode::INVALID_PARAMS,
message: Cow::from("Missing 'query' parameter"),
data: None,
})?;
let k = params.get("k").and_then(|v| v.as_u64()).unwrap_or(5) as usize;
@@ -93,29 +97,38 @@ impl RouterToolSelector for VectorToolSelector {
// Check if provider supports embeddings
if !self.embedding_provider.supports_embeddings() {
return Err(ToolError::ExecutionError(
"Embedding provider does not support embeddings".to_string(),
));
return Err(ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from("Embedding provider does not support embeddings"),
data: None,
});
}
let embeddings = self
.embedding_provider
.create_embeddings(vec![query.to_string()])
.await
.map_err(|e| {
ToolError::ExecutionError(format!("Failed to generate query embedding: {}", e))
.map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!("Failed to generate query embedding: {}", e)),
data: None,
})?;
let query_embedding = embeddings
.into_iter()
.next()
.ok_or_else(|| ToolError::ExecutionError("No embedding returned".to_string()))?;
let query_embedding = embeddings.into_iter().next().ok_or_else(|| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from("No embedding returned"),
data: None,
})?;
let vector_db = self.vector_db.read().await;
let tools = vector_db
.search_tools(query_embedding, k, extension_name)
.await
.map_err(|e| ToolError::ExecutionError(format!("Failed to search tools: {}", e)))?;
.map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!("Failed to search tools: {}", e)),
data: None,
})?;
let selected_tools: Vec<Content> = tools
.into_iter()
@@ -131,7 +144,7 @@ impl RouterToolSelector for VectorToolSelector {
Ok(selected_tools)
}
async fn index_tools(&self, tools: &[Tool], extension_name: &str) -> Result<(), ToolError> {
async fn index_tools(&self, tools: &[Tool], extension_name: &str) -> Result<(), ErrorData> {
let texts_to_embed: Vec<String> = tools
.iter()
.map(|tool| {
@@ -150,17 +163,21 @@ impl RouterToolSelector for VectorToolSelector {
.collect();
if !self.embedding_provider.supports_embeddings() {
return Err(ToolError::ExecutionError(
"Embedding provider does not support embeddings".to_string(),
));
return Err(ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from("Embedding provider does not support embeddings"),
data: None,
});
}
let embeddings = self
.embedding_provider
.create_embeddings(texts_to_embed)
.await
.map_err(|e| {
ToolError::ExecutionError(format!("Failed to generate tool embeddings: {}", e))
.map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!("Failed to generate tool embeddings: {}", e)),
data: None,
})?;
// Create tool records
@@ -194,8 +211,10 @@ impl RouterToolSelector for VectorToolSelector {
let existing_tools = vector_db
.search_tools(record.vector.clone(), 1, Some(&record.extension_name))
.await
.map_err(|e| {
ToolError::ExecutionError(format!("Failed to search for existing tools: {}", e))
.map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!("Failed to search for existing tools: {}", e)),
data: None,
})?;
// Only add if no exact match found
@@ -212,21 +231,30 @@ impl RouterToolSelector for VectorToolSelector {
vector_db
.index_tools(new_tool_records)
.await
.map_err(|e| ToolError::ExecutionError(format!("Failed to index tools: {}", e)))?;
.map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!("Failed to index tools: {}", e)),
data: None,
})?;
}
Ok(())
}
async fn remove_tool(&self, tool_name: &str) -> Result<(), ToolError> {
async fn remove_tool(&self, tool_name: &str) -> Result<(), ErrorData> {
let vector_db = self.vector_db.read().await;
vector_db.remove_tool(tool_name).await.map_err(|e| {
ToolError::ExecutionError(format!("Failed to remove tool {}: {}", tool_name, e))
})?;
vector_db
.remove_tool(tool_name)
.await
.map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!("Failed to remove tool {}: {}", tool_name, e)),
data: None,
})?;
Ok(())
}
async fn record_tool_call(&self, tool_name: &str) -> Result<(), ToolError> {
async fn record_tool_call(&self, tool_name: &str) -> Result<(), ErrorData> {
let mut recent_calls = self.recent_tool_calls.write().await;
if recent_calls.len() >= 100 {
recent_calls.pop_front();
@@ -235,7 +263,7 @@ impl RouterToolSelector for VectorToolSelector {
Ok(())
}
async fn get_recent_tool_calls(&self, limit: usize) -> Result<Vec<String>, ToolError> {
async fn get_recent_tool_calls(&self, limit: usize) -> Result<Vec<String>, ErrorData> {
let recent_calls = self.recent_tool_calls.read().await;
Ok(recent_calls.iter().rev().take(limit).cloned().collect())
}
@@ -263,11 +291,15 @@ impl LLMToolSelector {
#[async_trait]
impl RouterToolSelector for LLMToolSelector {
async fn select_tools(&self, params: Value) -> Result<Vec<Content>, ToolError> {
async fn select_tools(&self, params: Value) -> Result<Vec<Content>, ErrorData> {
let query = params
.get("query")
.and_then(|v| v.as_str())
.ok_or_else(|| ToolError::InvalidParameters("Missing 'query' parameter".to_string()))?;
.ok_or_else(|| ErrorData {
code: ErrorCode::INVALID_PARAMS,
message: Cow::from("Missing 'query' parameter"),
data: None,
})?;
let extension_name = params
.get("extension_name")
@@ -297,8 +329,10 @@ impl RouterToolSelector for LLMToolSelector {
};
let user_prompt =
render_global_file("router_tool_selector.md", &context).map_err(|e| {
ToolError::ExecutionError(format!("Failed to render prompt template: {}", e))
render_global_file("router_tool_selector.md", &context).map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!("Failed to render prompt template: {}", e)),
data: None,
})?;
let user_message = Message::user().with_text(&user_prompt);
@@ -306,7 +340,11 @@ impl RouterToolSelector for LLMToolSelector {
.llm_provider
.complete("", &[user_message], &[])
.await
.map_err(|e| ToolError::ExecutionError(format!("Failed to search tools: {}", e)))?;
.map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!("Failed to search tools: {}", e)),
data: None,
})?;
// Extract just the message content from the response
let (message, _usage) = response;
@@ -325,7 +363,7 @@ impl RouterToolSelector for LLMToolSelector {
}
}
async fn index_tools(&self, tools: &[Tool], extension_name: &str) -> Result<(), ToolError> {
async fn index_tools(&self, tools: &[Tool], extension_name: &str) -> Result<(), ErrorData> {
let mut tool_strings = self.tool_strings.write().await;
for tool in tools {
@@ -354,7 +392,7 @@ impl RouterToolSelector for LLMToolSelector {
Ok(())
}
async fn remove_tool(&self, tool_name: &str) -> Result<(), ToolError> {
async fn remove_tool(&self, tool_name: &str) -> Result<(), ErrorData> {
let mut tool_strings = self.tool_strings.write().await;
if let Some(extension_name) = tool_name.split("__").next() {
tool_strings.remove(extension_name);
@@ -362,7 +400,7 @@ impl RouterToolSelector for LLMToolSelector {
Ok(())
}
async fn record_tool_call(&self, tool_name: &str) -> Result<(), ToolError> {
async fn record_tool_call(&self, tool_name: &str) -> Result<(), ErrorData> {
let mut recent_calls = self.recent_tool_calls.write().await;
if recent_calls.len() >= 100 {
recent_calls.pop_front();
@@ -371,7 +409,7 @@ impl RouterToolSelector for LLMToolSelector {
Ok(())
}
async fn get_recent_tool_calls(&self, limit: usize) -> Result<Vec<String>, ToolError> {
async fn get_recent_tool_calls(&self, limit: usize) -> Result<Vec<String>, ErrorData> {
let recent_calls = self.recent_tool_calls.read().await;
Ok(recent_calls.iter().rev().take(limit).cloned().collect())
}
+189 -93
View File
@@ -6,8 +6,8 @@
use std::sync::Arc;
use chrono::Utc;
use mcp_core::{ToolError, ToolResult};
use rmcp::model::Content;
use mcp_core::ToolResult;
use rmcp::model::{Content, ErrorCode, ErrorData};
use crate::recipe::Recipe;
use crate::scheduler_trait::SchedulerTrait;
@@ -24,8 +24,10 @@ impl Agent {
let scheduler = match self.scheduler_service.lock().await.as_ref() {
Some(s) => s.clone(),
None => {
return Err(ToolError::ExecutionError(
return Err(ErrorData::new(
ErrorCode::INTERNAL_ERROR,
"Scheduler not available. This tool only works in server mode.".to_string(),
None,
))
}
};
@@ -33,7 +35,13 @@ impl Agent {
let action = arguments
.get("action")
.and_then(|v| v.as_str())
.ok_or_else(|| ToolError::ExecutionError("Missing 'action' parameter".to_string()))?;
.ok_or_else(|| {
ErrorData::new(
ErrorCode::INVALID_PARAMS,
"Missing 'action' parameter".to_string(),
None,
)
})?;
match action {
"list" => self.handle_list_jobs(scheduler).await,
@@ -46,10 +54,11 @@ impl Agent {
"inspect" => self.handle_inspect_job(scheduler, arguments).await,
"sessions" => self.handle_list_sessions(scheduler, arguments).await,
"session_content" => self.handle_session_content(arguments).await,
_ => Err(ToolError::ExecutionError(format!(
"Unknown action: {}",
action
))),
_ => Err(ErrorData::new(
ErrorCode::INTERNAL_ERROR,
format!("Unknown action: {}", action),
None,
)),
}
}
@@ -61,17 +70,22 @@ impl Agent {
match scheduler.list_scheduled_jobs().await {
Ok(jobs) => {
let jobs_json = serde_json::to_string_pretty(&jobs).map_err(|e| {
ToolError::ExecutionError(format!("Failed to serialize jobs: {}", e))
ErrorData::new(
ErrorCode::INTERNAL_ERROR,
format!("Failed to serialize jobs: {}", e),
None,
)
})?;
Ok(vec![Content::text(format!(
"Scheduled Jobs:\n{}",
jobs_json
))])
}
Err(e) => Err(ToolError::ExecutionError(format!(
"Failed to list jobs: {}",
e
))),
Err(e) => Err(ErrorData::new(
ErrorCode::INTERNAL_ERROR,
format!("Failed to list jobs: {}", e),
None,
)),
}
}
@@ -85,14 +99,22 @@ impl Agent {
.get("recipe_path")
.and_then(|v| v.as_str())
.ok_or_else(|| {
ToolError::ExecutionError("Missing 'recipe_path' parameter".to_string())
ErrorData::new(
ErrorCode::INVALID_PARAMS,
"Missing 'recipe_path' parameter".to_string(),
None,
)
})?;
let cron_expression = arguments
.get("cron_expression")
.and_then(|v| v.as_str())
.ok_or_else(|| {
ToolError::ExecutionError("Missing 'cron_expression' parameter".to_string())
ErrorData::new(
ErrorCode::INVALID_PARAMS,
"Missing 'cron_expression' parameter".to_string(),
None,
)
})?;
// Get the execution_mode parameter, defaulting to "background" if not provided
@@ -103,18 +125,23 @@ impl Agent {
// Validate execution_mode is either "foreground" or "background"
if execution_mode != "foreground" && execution_mode != "background" {
return Err(ToolError::ExecutionError(format!(
"Invalid execution_mode: {}. Must be 'foreground' or 'background'",
execution_mode
)));
return Err(ErrorData::new(
ErrorCode::INTERNAL_ERROR,
format!(
"Invalid execution_mode: {}. Must be 'foreground' or 'background'",
execution_mode
),
None,
));
}
// Validate recipe file exists and is readable
if !std::path::Path::new(recipe_path).exists() {
return Err(ToolError::ExecutionError(format!(
"Recipe file not found: {}",
recipe_path
)));
return Err(ErrorData::new(
ErrorCode::INTERNAL_ERROR,
format!("Recipe file not found: {}", recipe_path),
None,
));
}
// Validate it's a valid recipe by trying to parse it
@@ -122,19 +149,28 @@ impl Agent {
Ok(content) => {
if recipe_path.ends_with(".json") {
serde_json::from_str::<Recipe>(&content).map_err(|e| {
ToolError::ExecutionError(format!("Invalid JSON recipe: {}", e))
ErrorData::new(
ErrorCode::INTERNAL_ERROR,
format!("Invalid JSON recipe: {}", e),
None,
)
})?;
} else {
serde_yaml::from_str::<Recipe>(&content).map_err(|e| {
ToolError::ExecutionError(format!("Invalid YAML recipe: {}", e))
ErrorData::new(
ErrorCode::INTERNAL_ERROR,
format!("Invalid YAML recipe: {}", e),
None,
)
})?;
}
}
Err(e) => {
return Err(ToolError::ExecutionError(format!(
"Cannot read recipe file: {}",
e
)))
return Err(ErrorData::new(
ErrorCode::INTERNAL_ERROR,
format!("Cannot read recipe file: {}", e),
None,
))
}
}
@@ -158,10 +194,11 @@ impl Agent {
"Successfully created scheduled job '{}' for recipe '{}' with cron expression '{}' in {} mode",
job_id, recipe_path, cron_expression, execution_mode
))]),
Err(e) => Err(ToolError::ExecutionError(format!(
"Failed to create job: {}",
e
))),
Err(e) => Err(ErrorData::new(
ErrorCode::INTERNAL_ERROR,
format!("Failed to create job: {}", e),
None,
)),
}
}
@@ -174,17 +211,24 @@ impl Agent {
let job_id = arguments
.get("job_id")
.and_then(|v| v.as_str())
.ok_or_else(|| ToolError::ExecutionError("Missing 'job_id' parameter".to_string()))?;
.ok_or_else(|| {
ErrorData::new(
ErrorCode::INVALID_PARAMS,
"Missing 'job_id' parameter".to_string(),
None,
)
})?;
match scheduler.run_now(job_id).await {
Ok(session_id) => Ok(vec![Content::text(format!(
"Successfully started job '{}'. Session ID: {}",
job_id, session_id
))]),
Err(e) => Err(ToolError::ExecutionError(format!(
"Failed to run job: {}",
e
))),
Err(e) => Err(ErrorData::new(
ErrorCode::INTERNAL_ERROR,
format!("Failed to run job: {}", e),
None,
)),
}
}
@@ -197,17 +241,24 @@ impl Agent {
let job_id = arguments
.get("job_id")
.and_then(|v| v.as_str())
.ok_or_else(|| ToolError::ExecutionError("Missing 'job_id' parameter".to_string()))?;
.ok_or_else(|| {
ErrorData::new(
ErrorCode::INTERNAL_ERROR,
"Missing 'job_id' parameter".to_string(),
None,
)
})?;
match scheduler.pause_schedule(job_id).await {
Ok(()) => Ok(vec![Content::text(format!(
"Successfully paused job '{}'",
job_id
))]),
Err(e) => Err(ToolError::ExecutionError(format!(
"Failed to pause job: {}",
e
))),
Err(e) => Err(ErrorData::new(
ErrorCode::INTERNAL_ERROR,
format!("Failed to pause job: {}", e),
None,
)),
}
}
@@ -220,17 +271,24 @@ impl Agent {
let job_id = arguments
.get("job_id")
.and_then(|v| v.as_str())
.ok_or_else(|| ToolError::ExecutionError("Missing 'job_id' parameter".to_string()))?;
.ok_or_else(|| {
ErrorData::new(
ErrorCode::INTERNAL_ERROR,
"Missing 'job_id' parameter".to_string(),
None,
)
})?;
match scheduler.unpause_schedule(job_id).await {
Ok(()) => Ok(vec![Content::text(format!(
"Successfully unpaused job '{}'",
job_id
))]),
Err(e) => Err(ToolError::ExecutionError(format!(
"Failed to unpause job: {}",
e
))),
Err(e) => Err(ErrorData::new(
ErrorCode::INTERNAL_ERROR,
format!("Failed to unpause job: {}", e),
None,
)),
}
}
@@ -243,17 +301,24 @@ impl Agent {
let job_id = arguments
.get("job_id")
.and_then(|v| v.as_str())
.ok_or_else(|| ToolError::ExecutionError("Missing 'job_id' parameter".to_string()))?;
.ok_or_else(|| {
ErrorData::new(
ErrorCode::INTERNAL_ERROR,
"Missing 'job_id' parameter".to_string(),
None,
)
})?;
match scheduler.remove_scheduled_job(job_id).await {
Ok(()) => Ok(vec![Content::text(format!(
"Successfully deleted job '{}'",
job_id
))]),
Err(e) => Err(ToolError::ExecutionError(format!(
"Failed to delete job: {}",
e
))),
Err(e) => Err(ErrorData::new(
ErrorCode::INTERNAL_ERROR,
format!("Failed to delete job: {}", e),
None,
)),
}
}
@@ -266,17 +331,24 @@ impl Agent {
let job_id = arguments
.get("job_id")
.and_then(|v| v.as_str())
.ok_or_else(|| ToolError::ExecutionError("Missing 'job_id' parameter".to_string()))?;
.ok_or_else(|| {
ErrorData::new(
ErrorCode::INTERNAL_ERROR,
"Missing 'job_id' parameter".to_string(),
None,
)
})?;
match scheduler.kill_running_job(job_id).await {
Ok(()) => Ok(vec![Content::text(format!(
"Successfully killed running job '{}'",
job_id
))]),
Err(e) => Err(ToolError::ExecutionError(format!(
"Failed to kill job: {}",
e
))),
Err(e) => Err(ErrorData::new(
ErrorCode::INTERNAL_ERROR,
format!("Failed to kill job: {}", e),
None,
)),
}
}
@@ -289,7 +361,13 @@ impl Agent {
let job_id = arguments
.get("job_id")
.and_then(|v| v.as_str())
.ok_or_else(|| ToolError::ExecutionError("Missing 'job_id' parameter".to_string()))?;
.ok_or_else(|| {
ErrorData::new(
ErrorCode::INTERNAL_ERROR,
"Missing 'job_id' parameter".to_string(),
None,
)
})?;
match scheduler.get_running_job_info(job_id).await {
Ok(Some((session_id, start_time))) => {
@@ -303,10 +381,11 @@ impl Agent {
"Job '{}' is not currently running",
job_id
))]),
Err(e) => Err(ToolError::ExecutionError(format!(
"Failed to inspect job: {}",
e
))),
Err(e) => Err(ErrorData::new(
ErrorCode::INTERNAL_ERROR,
format!("Failed to inspect job: {}", e),
None,
)),
}
}
@@ -319,7 +398,13 @@ impl Agent {
let job_id = arguments
.get("job_id")
.and_then(|v| v.as_str())
.ok_or_else(|| ToolError::ExecutionError("Missing 'job_id' parameter".to_string()))?;
.ok_or_else(|| {
ErrorData::new(
ErrorCode::INVALID_PARAMS,
"Missing 'job_id' parameter".to_string(),
None,
)
})?;
let limit = arguments
.get("limit")
@@ -353,10 +438,11 @@ impl Agent {
))])
}
}
Err(e) => Err(ToolError::ExecutionError(format!(
"Failed to list sessions: {}",
e
))),
Err(e) => Err(ErrorData::new(
ErrorCode::INTERNAL_ERROR,
format!("Failed to list sessions: {}", e),
None,
)),
}
}
@@ -369,7 +455,11 @@ impl Agent {
.get("session_id")
.and_then(|v| v.as_str())
.ok_or_else(|| {
ToolError::ExecutionError("Missing 'session_id' parameter".to_string())
ErrorData::new(
ErrorCode::INTERNAL_ERROR,
"Missing 'session_id' parameter".to_string(),
None,
)
})?;
// Get the session file path
@@ -378,29 +468,32 @@ impl Agent {
) {
Ok(path) => path,
Err(e) => {
return Err(ToolError::ExecutionError(format!(
"Invalid session ID '{}': {}",
session_id, e
)));
return Err(ErrorData::new(
ErrorCode::INTERNAL_ERROR,
format!("Invalid session ID '{}': {}", session_id, e),
None,
));
}
};
// Check if session file exists
if !session_path.exists() {
return Err(ToolError::ExecutionError(format!(
"Session '{}' not found",
session_id
)));
return Err(ErrorData::new(
ErrorCode::INTERNAL_ERROR,
format!("Session '{}' not found", session_id),
None,
));
}
// Read session metadata
let metadata = match crate::session::storage::read_metadata(&session_path) {
Ok(metadata) => metadata,
Err(e) => {
return Err(ToolError::ExecutionError(format!(
"Failed to read session metadata: {}",
e
)));
return Err(ErrorData::new(
ErrorCode::INTERNAL_ERROR,
format!("Failed to read session metadata: {}", e),
None,
));
}
};
@@ -408,10 +501,11 @@ impl Agent {
let messages = match crate::session::storage::read_messages(&session_path) {
Ok(messages) => messages,
Err(e) => {
return Err(ToolError::ExecutionError(format!(
"Failed to read session messages: {}",
e
)));
return Err(ErrorData::new(
ErrorCode::INTERNAL_ERROR,
format!("Failed to read session messages: {}", e),
None,
));
}
};
@@ -419,20 +513,22 @@ impl Agent {
let metadata_json = match serde_json::to_string_pretty(&metadata) {
Ok(json) => json,
Err(e) => {
return Err(ToolError::ExecutionError(format!(
"Failed to serialize metadata: {}",
e
)));
return Err(ErrorData::new(
ErrorCode::INTERNAL_ERROR,
format!("Failed to serialize metadata: {}", e),
None,
));
}
};
let messages_json = match serde_json::to_string_pretty(&messages) {
Ok(json) => json,
Err(e) => {
return Err(ToolError::ExecutionError(format!(
"Failed to serialize messages: {}",
e
)));
return Err(ErrorData::new(
ErrorCode::INTERNAL_ERROR,
format!("Failed to serialize messages: {}", e),
None,
));
}
};
+22 -10
View File
@@ -1,7 +1,7 @@
use mcp_core::ToolError;
use rmcp::model::Content;
use rmcp::model::Tool;
use rmcp::model::{Content, ErrorCode, ErrorData};
use serde_json::Value;
use std::borrow::Cow;
use std::collections::HashMap;
use crate::{
@@ -63,7 +63,11 @@ impl SubRecipeManager {
.await;
match result {
Ok(call_result) => ToolCallResult::from(Ok(call_result)),
Err(e) => ToolCallResult::from(Err(ToolError::ExecutionError(e.to_string()))),
Err(e) => ToolCallResult::from(Err(ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(e.to_string()),
data: None,
})),
}
}
@@ -72,25 +76,33 @@ impl SubRecipeManager {
tool_name: &str,
params: Value,
tasks_manager: &TasksManager,
) -> Result<Vec<Content>, ToolError> {
) -> Result<Vec<Content>, ErrorData> {
let sub_recipe = self.sub_recipes.get(tool_name).ok_or_else(|| {
let sub_recipe_name = tool_name
.strip_prefix(SUB_RECIPE_TASK_TOOL_NAME_PREFIX)
.and_then(|s| s.strip_prefix("_"))
.ok_or_else(|| {
ToolError::InvalidParameters(format!(
.ok_or_else(|| ErrorData {
code: ErrorCode::INVALID_PARAMS,
message: Cow::from(format!(
"Invalid sub-recipe tool name format: {}",
tool_name
))
)),
data: None,
})
.unwrap();
ToolError::InvalidParameters(format!("Sub-recipe '{}' not found", sub_recipe_name))
ErrorData {
code: ErrorCode::INVALID_PARAMS,
message: Cow::from(format!("Sub-recipe '{}' not found", sub_recipe_name)),
data: None,
}
})?;
let output = create_sub_recipe_task(sub_recipe, params, tasks_manager)
.await
.map_err(|e| {
ToolError::ExecutionError(format!("Sub-recipe task createion failed: {}", e))
.map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!("Sub-recipe task creation failed: {}", e)),
data: None,
})?;
Ok(vec![Content::text(output)])
}
+11 -3
View File
@@ -8,8 +8,8 @@ use crate::{
};
use anyhow::anyhow;
use chrono::{DateTime, Utc};
use mcp_core::handler::ToolError;
use rmcp::model::Tool;
use rmcp::model::{ErrorCode, ErrorData};
use serde::{Deserialize, Serialize};
// use serde_json::{self};
use crate::conversation::message::{Message, MessageContent, ToolRequest};
@@ -206,7 +206,11 @@ impl SubAgent {
.await
{
Ok(result) => result.result.await,
Err(e) => Err(ToolError::ExecutionError(e.to_string())),
Err(e) => Err(ErrorData::new(
ErrorCode::INTERNAL_ERROR,
e.to_string(),
None,
)),
};
match tool_result {
@@ -220,7 +224,11 @@ impl SubAgent {
// Create a user message with the tool error
let tool_error_message = Message::user().with_tool_response(
request.id.clone(),
Err(ToolError::ExecutionError(e.to_string())),
Err(ErrorData::new(
ErrorCode::INTERNAL_ERROR,
e.to_string(),
None,
)),
);
messages.push(tool_error_message);
}
@@ -1,5 +1,6 @@
use mcp_core::ToolError;
use rmcp::model::{Content, ServerNotification, Tool, ToolAnnotations};
use std::borrow::Cow;
use rmcp::model::{Content, ErrorCode, ErrorData, ServerNotification, Tool, ToolAnnotations};
use serde_json::Value;
use crate::agents::subagent_task_config::TaskConfig;
@@ -90,7 +91,11 @@ pub async fn run_tasks(
let output = serde_json::to_string(&result).unwrap();
Ok(vec![Content::text(output)])
}
Err(e) => Err(ToolError::ExecutionError(e.to_string())),
Err(e) => Err(ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(e.to_string()),
data: None,
}),
}
};
+8 -4
View File
@@ -1,7 +1,7 @@
use crate::agents::subagent::SubAgent;
use crate::agents::subagent_task_config::TaskConfig;
use anyhow::Result;
use mcp_core::ToolError;
use rmcp::model::{ErrorCode, ErrorData};
/// Standalone function to run a complete subagent task
pub async fn run_complete_subagent_task(
@@ -9,9 +9,13 @@ pub async fn run_complete_subagent_task(
task_config: TaskConfig,
) -> Result<String, anyhow::Error> {
// Create the subagent with the parent agent's provider
let subagent = SubAgent::new(task_config.clone())
.await
.map_err(|e| ToolError::ExecutionError(format!("Failed to create subagent: {}", e)))?;
let subagent = SubAgent::new(task_config.clone()).await.map_err(|e| {
ErrorData::new(
ErrorCode::INTERNAL_ERROR,
format!("Failed to create subagent: {}", e),
None,
)
})?;
// Execute the subagent task
let messages = subagent
+10 -8
View File
@@ -10,8 +10,7 @@ use crate::config::Config;
use crate::conversation::message::ToolRequest;
use crate::providers::base::Provider;
use anyhow::{anyhow, Result};
use mcp_core::ToolError;
use rmcp::model::Tool;
use rmcp::model::{ErrorCode, ErrorData, Tool};
use serde_json::Value;
use std::sync::Arc;
use tokio::sync::Mutex;
@@ -52,18 +51,21 @@ impl ToolRouteManager {
pub async fn dispatch_route_search_tool(
&self,
arguments: Value,
) -> Result<ToolCallResult, ToolError> {
) -> Result<ToolCallResult, ErrorData> {
let selector = self.router_tool_selector.lock().await.clone();
match selector.as_ref() {
Some(selector) => match selector.select_tools(arguments).await {
Ok(tools) => Ok(ToolCallResult::from(Ok(tools))),
Err(e) => Err(ToolError::ExecutionError(format!(
"Failed to select tools: {}",
e
))),
Err(e) => Err(ErrorData::new(
ErrorCode::INTERNAL_ERROR,
format!("Failed to select tools: {}", e),
None,
)),
},
None => Err(ToolError::ExecutionError(
None => Err(ErrorData::new(
ErrorCode::INTERNAL_ERROR,
"No tool selector available".to_string(),
None,
)),
}
}
+7 -5
View File
@@ -603,12 +603,12 @@ impl Message {
mod tests {
use crate::conversation::message::{Message, MessageContent};
use crate::conversation::*;
use mcp_core::handler::ToolError;
use mcp_core::ToolCall;
use rmcp::model::{
AnnotateAble, PromptMessage, PromptMessageContent, PromptMessageRole, RawEmbeddedResource,
RawImageContent, ResourceContents,
};
use rmcp::model::{ErrorCode, ErrorData};
use serde_json::{json, Value};
#[test]
@@ -683,9 +683,11 @@ mod tests {
fn test_error_serialization() {
let message = Message::assistant().with_tool_request(
"tool123",
Err(ToolError::ExecutionError(
"Something went wrong".to_string(),
)),
Err(ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: std::borrow::Cow::from("Something went wrong".to_string()),
data: None,
}),
);
let json_str = serde_json::to_string_pretty(&message).unwrap();
@@ -697,7 +699,7 @@ mod tests {
// Check tool_call serialization with error
let tool_call = &value["content"][0]["toolCall"];
assert_eq!(tool_call["status"], "error");
assert_eq!(tool_call["error"], "Execution failed: Something went wrong");
assert_eq!(tool_call["error"], "-32603: Something went wrong");
}
#[test]
@@ -1,6 +1,8 @@
use mcp_core::handler::{ToolError, ToolResult};
use mcp_core::ToolResult;
use rmcp::model::{ErrorCode, ErrorData};
use serde::ser::SerializeStruct;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::borrow::Cow;
pub fn serialize<T, S>(value: &ToolResult<T>, serializer: S) -> Result<S::Ok, S::Error>
where
@@ -52,7 +54,11 @@ where
}
ResultFormat::Error { status, error } => {
if status == "error" {
Ok(Err(ToolError::ExecutionError(error)))
Ok(Err(ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(error),
data: None,
}))
} else {
Err(serde::de::Error::custom(format!(
"Expected status 'error', got '{}'",
@@ -3,8 +3,8 @@ use crate::model::ModelConfig;
use crate::providers::base::Usage;
use crate::providers::errors::ProviderError;
use anyhow::{anyhow, Result};
use mcp_core::tool::ToolCall;
use rmcp::model::{Role, Tool};
use mcp_core::ToolCall;
use rmcp::model::{ErrorCode, ErrorData, Role, Tool};
use serde_json::{json, Value};
use std::collections::HashSet;
@@ -572,8 +572,10 @@ where
Ok(parsed) => parsed,
Err(_) => {
// If parsing fails, create an error tool request
let error = mcp_core::handler::ToolError::InvalidParameters(
format!("Could not parse tool arguments: {}", args)
let error = ErrorData::new(
ErrorCode::INVALID_PARAMS,
format!("Could not parse tool arguments: {}", args),
None,
);
let mut message = Message::new(
Role::Assistant,
@@ -985,7 +987,7 @@ mod tests {
#[test]
fn test_tool_error_handling_maintains_pairing() {
use crate::conversation::message::Message;
use mcp_core::handler::ToolError;
use rmcp::model::{ErrorCode, ErrorData};
let messages = vec![
Message::assistant().with_tool_request(
@@ -994,7 +996,11 @@ mod tests {
),
Message::user().with_tool_response(
"tool_1",
Err(ToolError::ExecutionError("Tool failed".to_string())),
Err(ErrorData::new(
ErrorCode::INTERNAL_ERROR,
"Tool failed".to_string(),
None,
)),
),
];
@@ -1012,7 +1018,7 @@ mod tests {
assert_eq!(spec[1]["content"][0]["tool_use_id"], "tool_1");
assert_eq!(
spec[1]["content"][0]["content"],
"Error: Execution failed: Tool failed"
"Error: -32603: Tool failed"
);
assert_eq!(spec[1]["content"][0]["is_error"], true);
}
+13 -8
View File
@@ -1,3 +1,4 @@
use std::borrow::Cow;
use std::collections::HashMap;
use std::path::Path;
@@ -6,8 +7,8 @@ use aws_sdk_bedrockruntime::types as bedrock;
use aws_smithy_types::{Document, Number};
use base64::Engine;
use chrono::Utc;
use mcp_core::{ToolCall, ToolError, ToolResult};
use rmcp::model::{Content, RawContent, ResourceContents, Role, Tool};
use mcp_core::{ToolCall, ToolResult};
use rmcp::model::{Content, ErrorCode, ErrorData, RawContent, ResourceContents, Role, Tool};
use serde_json::Value;
use super::super::base::Usage;
@@ -286,9 +287,11 @@ pub fn from_bedrock_content_block(block: &bedrock::ContentBlock) -> Result<Messa
bedrock::ContentBlock::ToolResult(tool_res) => MessageContent::tool_response(
tool_res.tool_use_id.to_string(),
if tool_res.content.is_empty() {
Err(ToolError::ExecutionError(
"Empty content for tool use from Bedrock".to_string(),
))
Err(ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from("Empty content for tool use from Bedrock".to_string()),
data: None,
})
} else {
tool_res
.content
@@ -307,9 +310,11 @@ pub fn from_bedrock_tool_result_content_block(
Ok(match content {
bedrock::ToolResultContentBlock::Text(text) => Content::text(text.to_string()),
_ => {
return Err(ToolError::ExecutionError(
"Unsupported tool result from Bedrock".to_string(),
))
return Err(ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from("Unsupported tool result from Bedrock".to_string()),
data: None,
})
}
})
}
@@ -5,10 +5,13 @@ use crate::providers::utils::{
sanitize_function_name, ImageFormat,
};
use anyhow::{anyhow, Error};
use mcp_core::{ToolCall, ToolError};
use rmcp::model::{AnnotateAble, Content, RawContent, ResourceContents, Role, Tool};
use mcp_core::ToolCall;
use rmcp::model::{
AnnotateAble, Content, ErrorCode, ErrorData, RawContent, ResourceContents, Role, Tool,
};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use std::borrow::Cow;
#[derive(Serialize)]
struct DatabricksMessage {
@@ -356,10 +359,14 @@ pub fn response_to_message(response: &Value) -> anyhow::Result<Message> {
};
if !is_valid_function_name(&function_name) {
let error = ToolError::NotFound(format!(
"The provided function name '{}' had invalid characters, it must match this regex [a-zA-Z0-9_-]+",
function_name
));
let error = ErrorData {
code: ErrorCode::INVALID_REQUEST,
message: Cow::from(format!(
"The provided function name '{}' had invalid characters, it must match this regex [a-zA-Z0-9_-]+",
function_name
)),
data: None,
};
content.push(MessageContent::tool_request(id, Err(error)));
} else {
match safely_parse_json(&arguments_str) {
@@ -370,10 +377,14 @@ pub fn response_to_message(response: &Value) -> anyhow::Result<Message> {
));
}
Err(e) => {
let error = ToolError::InvalidParameters(format!(
"Could not interpret tool use parameters for id {}: {}. Raw arguments: '{}'",
id, e, arguments_str
));
let error = ErrorData {
code: ErrorCode::INVALID_PARAMS,
message: Cow::from(format!(
"Could not interpret tool use parameters for id {}: {}. Raw arguments: '{}'",
id, e, arguments_str
)),
data: None,
};
content.push(MessageContent::tool_request(id, Err(error)));
}
}
@@ -963,7 +974,11 @@ mod tests {
if let MessageContent::ToolRequest(request) = &message.content[0] {
match &request.tool_call {
Err(ToolError::NotFound(msg)) => {
Err(ErrorData {
code: ErrorCode::INVALID_REQUEST,
message: msg,
data: None,
}) => {
assert!(msg.starts_with("The provided function name"));
}
_ => panic!("Expected ToolNotFound error"),
@@ -985,7 +1000,11 @@ mod tests {
if let MessageContent::ToolRequest(request) = &message.content[0] {
match &request.tool_call {
Err(ToolError::InvalidParameters(msg)) => {
Err(ErrorData {
code: ErrorCode::INVALID_PARAMS,
message: msg,
data: None,
}) => {
assert!(msg.starts_with("Could not interpret tool use parameters"));
}
_ => panic!("Expected InvalidParameters error"),
+19 -7
View File
@@ -3,9 +3,10 @@ use crate::providers::base::Usage;
use crate::providers::errors::ProviderError;
use crate::providers::utils::{is_valid_function_name, sanitize_function_name};
use anyhow::Result;
use mcp_core::tool::ToolCall;
use mcp_core::ToolCall;
use rand::{distributions::Alphanumeric, Rng};
use rmcp::model::{AnnotateAble, RawContent, Role, Tool};
use rmcp::model::{AnnotateAble, ErrorCode, ErrorData, RawContent, Role, Tool};
use std::borrow::Cow;
use crate::conversation::message::{Message, MessageContent};
use serde_json::{json, Map, Value};
@@ -254,10 +255,14 @@ pub fn response_to_message(response: Value) -> Result<Message> {
.unwrap_or_default()
.to_string();
if !is_valid_function_name(&name) {
let error = mcp_core::ToolError::NotFound(format!(
"The provided function name '{}' had invalid characters, it must match this regex [a-zA-Z0-9_-]+",
name
));
let error = ErrorData {
code: ErrorCode::INVALID_REQUEST,
message: Cow::from(format!(
"The provided function name '{}' had invalid characters, it must match this regex [a-zA-Z0-9_-]+",
name
)),
data: None,
};
content.push(MessageContent::tool_request(id, Err(error)));
} else {
let parameters = function_call.get("args");
@@ -741,7 +746,14 @@ mod tests {
assert_eq!(message.role, Role::Assistant);
assert_eq!(message.content.len(), 1);
if let Err(error) = &message.content[0].as_tool_request().unwrap().tool_call {
assert!(matches!(error, mcp_core::ToolError::NotFound(_)));
assert!(matches!(
error,
ErrorData {
code: ErrorCode::INVALID_REQUEST,
message: _,
data: None,
}
));
} else {
panic!("Expected tool request error");
}
+39 -16
View File
@@ -8,10 +8,13 @@ use crate::providers::utils::{
use anyhow::{anyhow, Error};
use async_stream::try_stream;
use futures::Stream;
use mcp_core::{ToolCall, ToolError};
use rmcp::model::{AnnotateAble, Content, RawContent, ResourceContents, Role, Tool};
use mcp_core::ToolCall;
use rmcp::model::{
AnnotateAble, Content, ErrorCode, ErrorData, RawContent, ResourceContents, Role, Tool,
};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use std::borrow::Cow;
use std::ops::Deref;
#[derive(Serialize, Deserialize, Debug)]
@@ -299,10 +302,14 @@ pub fn response_to_message(response: &Value) -> anyhow::Result<Message> {
};
if !is_valid_function_name(&function_name) {
let error = ToolError::NotFound(format!(
"The provided function name '{}' had invalid characters, it must match this regex [a-zA-Z0-9_-]+",
function_name
));
let error = ErrorData {
code: ErrorCode::INVALID_REQUEST,
message: Cow::from(format!(
"The provided function name '{}' had invalid characters, it must match this regex [a-zA-Z0-9_-]+",
function_name
)),
data: None,
};
content.push(MessageContent::tool_request(id, Err(error)));
} else {
match safely_parse_json(&arguments_str) {
@@ -313,10 +320,14 @@ pub fn response_to_message(response: &Value) -> anyhow::Result<Message> {
));
}
Err(e) => {
let error = ToolError::InvalidParameters(format!(
"Could not interpret tool use parameters for id {}: {}. Raw arguments: '{}'",
id, e, arguments_str
));
let error = ErrorData {
code: ErrorCode::INVALID_PARAMS,
message: Cow::from(format!(
"Could not interpret tool use parameters for id {}: {}. Raw arguments: '{}'",
id, e, arguments_str
)),
data: None,
};
content.push(MessageContent::tool_request(id, Err(error)));
}
}
@@ -508,10 +519,14 @@ where
)
},
Err(e) => {
let error = ToolError::InvalidParameters(format!(
"Could not interpret tool use parameters for id {}: {}",
id, e
));
let error = ErrorData {
code: ErrorCode::INVALID_PARAMS,
message: Cow::from(format!(
"Could not interpret tool use parameters for id {}: {}",
id, e
)),
data: None,
};
MessageContent::tool_request(id.clone(), Err(error))
}
};
@@ -991,7 +1006,11 @@ mod tests {
if let MessageContent::ToolRequest(request) = &message.content[0] {
match &request.tool_call {
Err(ToolError::NotFound(msg)) => {
Err(ErrorData {
code: ErrorCode::INVALID_REQUEST,
message: msg,
data: None,
}) => {
assert!(msg.starts_with("The provided function name"));
}
_ => panic!("Expected ToolNotFound error"),
@@ -1013,7 +1032,11 @@ mod tests {
if let MessageContent::ToolRequest(request) = &message.content[0] {
match &request.tool_call {
Err(ToolError::InvalidParameters(msg)) => {
Err(ErrorData {
code: ErrorCode::INVALID_PARAMS,
message: msg,
data: None,
}) => {
assert!(msg.starts_with("Could not interpret tool use parameters"));
}
_ => panic!("Expected InvalidParameters error"),
+43 -33
View File
@@ -1,6 +1,6 @@
#![cfg(test)]
use mcp_core::ToolError;
use rmcp::model::ErrorCode;
use serde_json::json;
use goose::agents::platform_tools::PLATFORM_MANAGE_SCHEDULE_TOOL_NAME;
@@ -94,9 +94,10 @@ async fn test_schedule_tool_list_action_error() {
.await;
assert!(result.is_err());
if let Err(ToolError::ExecutionError(msg)) = result {
assert!(msg.contains("Failed to list jobs"));
assert!(msg.contains("Database error"));
if let Err(err) = result {
assert_eq!(err.code, ErrorCode::INTERNAL_ERROR);
assert!(err.message.contains("Failed to list jobs"));
assert!(err.message.contains("Database error"));
} else {
panic!("Expected ExecutionError");
}
@@ -153,8 +154,9 @@ async fn test_schedule_tool_create_action_missing_params() {
.await;
assert!(result.is_err());
if let Err(ToolError::ExecutionError(msg)) = result {
assert!(msg.contains("Missing 'recipe_path' parameter"));
if let Err(err) = result {
assert_eq!(err.code, ErrorCode::INVALID_PARAMS);
assert!(err.message.contains("Missing 'recipe_path' parameter"));
} else {
panic!("Expected ExecutionError");
}
@@ -171,8 +173,9 @@ async fn test_schedule_tool_create_action_missing_params() {
.await;
assert!(result.is_err());
if let Err(ToolError::ExecutionError(msg)) = result {
assert!(msg.contains("Missing 'cron_expression' parameter"));
if let Err(err) = result {
assert_eq!(err.code, ErrorCode::INVALID_PARAMS);
assert!(err.message.contains("Missing 'cron_expression' parameter"));
} else {
panic!("Expected ExecutionError");
}
@@ -194,8 +197,9 @@ async fn test_schedule_tool_create_action_nonexistent_recipe() {
.await;
assert!(result.is_err());
if let Err(ToolError::ExecutionError(msg)) = result {
assert!(msg.contains("Recipe file not found"));
if let Err(err) = result {
assert_eq!(err.code, ErrorCode::INTERNAL_ERROR);
assert!(err.message.contains("Recipe file not found"));
} else {
panic!("Expected ExecutionError");
}
@@ -220,8 +224,9 @@ async fn test_schedule_tool_create_action_invalid_recipe() {
.await;
assert!(result.is_err());
if let Err(ToolError::ExecutionError(msg)) = result {
assert!(msg.contains("Invalid JSON recipe"));
if let Err(err) = result {
assert_eq!(err.code, ErrorCode::INTERNAL_ERROR);
assert!(err.message.contains("Invalid JSON recipe"));
} else {
panic!("Expected ExecutionError");
}
@@ -253,9 +258,10 @@ async fn test_schedule_tool_create_action_scheduler_error() {
.await;
assert!(result.is_err());
if let Err(ToolError::ExecutionError(msg)) = result {
assert!(msg.contains("Failed to create job"));
assert!(msg.contains("job1"));
if let Err(err) = result {
assert_eq!(err.code, ErrorCode::INTERNAL_ERROR);
assert!(err.message.contains("Failed to create job"));
assert!(err.message.contains("job1"));
} else {
panic!("Expected ExecutionError");
}
@@ -311,8 +317,9 @@ async fn test_schedule_tool_run_now_action_missing_job_id() {
.await;
assert!(result.is_err());
if let Err(ToolError::ExecutionError(msg)) = result {
assert!(msg.contains("Missing 'job_id' parameter"));
if let Err(err) = result {
assert_eq!(err.code, ErrorCode::INVALID_PARAMS);
assert!(err.message.contains("Missing 'job_id' parameter"));
} else {
panic!("Expected ExecutionError");
}
@@ -337,9 +344,9 @@ async fn test_schedule_tool_run_now_action_nonexistent_job() {
.await;
assert!(result.is_err());
if let Err(ToolError::ExecutionError(msg)) = result {
assert!(msg.contains("Failed to run job"));
assert!(msg.contains("nonexistent"));
if let Err(err) = result {
assert!(err.message.contains("Failed to run job"));
assert!(err.message.contains("nonexistent"));
} else {
panic!("Expected ExecutionError");
}
@@ -391,10 +398,11 @@ async fn test_schedule_tool_pause_action_missing_job_id() {
let result = agent
.handle_schedule_management(arguments, "test_req".to_string())
.await;
assert!(result.is_err());
if let Err(ToolError::ExecutionError(msg)) = result {
assert!(msg.contains("Missing 'job_id' parameter"));
if let Err(err) = result {
assert!(err.message.contains("Missing 'job_id' parameter"));
} else {
panic!("Expected ExecutionError");
}
@@ -422,9 +430,9 @@ async fn test_schedule_tool_pause_action_running_job() {
.await;
assert!(result.is_err());
if let Err(ToolError::ExecutionError(msg)) = result {
assert!(msg.contains("Failed to pause job"));
assert!(msg.contains("job1"));
if let Err(err) = result {
assert!(err.message.contains("Failed to pause job"));
assert!(err.message.contains("job1"));
} else {
panic!("Expected ExecutionError");
}
@@ -551,8 +559,8 @@ async fn test_schedule_tool_kill_action_not_running() {
.await;
assert!(result.is_err());
if let Err(ToolError::ExecutionError(msg)) = result {
assert!(msg.contains("Failed to kill job"));
if let Err(err) = result {
assert!(err.message.contains("Failed to kill job"));
} else {
panic!("Expected ExecutionError");
}
@@ -764,8 +772,10 @@ async fn test_schedule_tool_session_content_action() {
.await;
assert!(result.is_err());
if let Err(ToolError::ExecutionError(msg)) = result {
assert!(msg.contains("Session 'non_existent_session' not found"));
if let Err(err) = result {
assert!(err
.message
.contains("Session 'non_existent_session' not found"));
} else {
panic!("Expected ExecutionError");
}
@@ -840,8 +850,8 @@ async fn test_schedule_tool_session_content_action_missing_session_id() {
.await;
assert!(result.is_err());
if let Err(ToolError::ExecutionError(msg)) = result {
assert!(msg.contains("Missing 'session_id' parameter"));
if let Err(err) = result {
assert!(err.message.contains("Missing 'session_id' parameter"));
} else {
panic!("Expected ExecutionError");
}
@@ -861,8 +871,8 @@ async fn test_schedule_tool_unknown_action() {
.await;
assert!(result.is_err());
if let Err(ToolError::ExecutionError(msg)) = result {
assert!(msg.contains("Unknown action"));
if let Err(err) = result {
assert!(err.message.contains("Unknown action"));
} else {
panic!("Expected ExecutionError");
}
+30 -33
View File
@@ -1,22 +1,7 @@
use serde::{Deserialize, Serialize};
#[allow(unused_imports)] // this is used in schema below
use serde_json::{json, Value};
use rmcp::model::{ErrorCode, ErrorData};
use thiserror::Error;
#[non_exhaustive]
#[derive(Error, Debug, Clone, Deserialize, Serialize, PartialEq)]
pub enum ToolError {
#[error("Invalid parameters: {0}")]
InvalidParameters(String),
#[error("Execution failed: {0}")]
ExecutionError(String),
#[error("Schema error: {0}")]
SchemaError(String),
#[error("Tool not found: {0}")]
NotFound(String),
}
pub type ToolResult<T> = std::result::Result<T, ToolError>;
pub type ToolResult<T> = std::result::Result<T, ErrorData>;
#[derive(Error, Debug)]
pub enum ResourceError {
@@ -36,31 +21,43 @@ pub enum PromptError {
NotFound(String),
}
/// Helper function to require a string, returning a ToolError
/// Helper function to require a string, returning an ErrorData
pub fn require_str_parameter<'a>(
v: &'a serde_json::Value,
name: &str,
) -> Result<&'a str, ToolError> {
let v = v
.get(name)
.ok_or_else(|| ToolError::InvalidParameters(format!("The parameter {name} is required")))?;
) -> Result<&'a str, ErrorData> {
let v = v.get(name).ok_or_else(|| {
ErrorData::new(
ErrorCode::INVALID_PARAMS,
format!("The parameter {name} is required"),
None,
)
})?;
match v.as_str() {
Some(r) => Ok(r),
None => Err(ToolError::InvalidParameters(format!(
"The parameter {name} must be a string"
))),
None => Err(ErrorData::new(
ErrorCode::INVALID_PARAMS,
format!("The parameter {name} must be a string"),
None,
)),
}
}
/// Helper function to require a u64, returning a ToolError
pub fn require_u64_parameter(v: &serde_json::Value, name: &str) -> Result<u64, ToolError> {
let v = v
.get(name)
.ok_or_else(|| ToolError::InvalidParameters(format!("The parameter {name} is required")))?;
/// Helper function to require a u64, returning an ErrorData
pub fn require_u64_parameter(v: &serde_json::Value, name: &str) -> Result<u64, ErrorData> {
let v = v.get(name).ok_or_else(|| {
ErrorData::new(
ErrorCode::INVALID_PARAMS,
format!("The parameter {name} is required"),
None,
)
})?;
match v.as_u64() {
Some(r) => Ok(r),
None => Err(ToolError::InvalidParameters(format!(
"The parameter {name} must be a number"
))),
None => Err(ErrorData::new(
ErrorCode::INVALID_PARAMS,
format!("The parameter {name} is required"),
None,
)),
}
}
+1 -1
View File
@@ -2,4 +2,4 @@ pub mod handler;
pub mod tool;
pub use tool::{Tool, ToolCall};
pub mod protocol;
pub use handler::{ToolError, ToolResult};
pub use handler::ToolResult;
+13 -7
View File
@@ -1,13 +1,15 @@
use anyhow::Result;
use mcp_core::handler::{PromptError, ResourceError};
use mcp_core::{handler::ToolError, protocol::ServerCapabilities};
use mcp_core::protocol::ServerCapabilities;
use mcp_server::router::{CapabilitiesBuilder, RouterService};
use mcp_server::{ByteTransport, Router, Server};
use rmcp::model::{
Content, JsonRpcMessage, Prompt, PromptArgument, RawResource, Resource, Tool, ToolAnnotations,
Content, ErrorCode, ErrorData, JsonRpcMessage, Prompt, PromptArgument, RawResource, Resource,
Tool, ToolAnnotations,
};
use rmcp::object;
use serde_json::Value;
use std::borrow::Cow;
use std::{future::Future, pin::Pin, sync::Arc};
use tokio::sync::mpsc;
use tokio::{
@@ -30,19 +32,19 @@ impl CounterRouter {
}
}
async fn increment(&self) -> Result<i32, ToolError> {
async fn increment(&self) -> Result<i32, ErrorData> {
let mut counter = self.counter.lock().await;
*counter += 1;
Ok(*counter)
}
async fn decrement(&self) -> Result<i32, ToolError> {
async fn decrement(&self) -> Result<i32, ErrorData> {
let mut counter = self.counter.lock().await;
*counter -= 1;
Ok(*counter)
}
async fn get_value(&self) -> Result<i32, ToolError> {
async fn get_value(&self) -> Result<i32, ErrorData> {
let counter = self.counter.lock().await;
Ok(*counter)
}
@@ -127,7 +129,7 @@ impl Router for CounterRouter {
tool_name: &str,
_arguments: Value,
_notifier: mpsc::Sender<JsonRpcMessage>,
) -> Pin<Box<dyn Future<Output = Result<Vec<Content>, ToolError>> + Send + 'static>> {
) -> Pin<Box<dyn Future<Output = Result<Vec<Content>, ErrorData>> + Send + 'static>> {
let this = self.clone();
let tool_name = tool_name.to_string();
@@ -145,7 +147,11 @@ impl Router for CounterRouter {
let value = this.get_value().await?;
Ok(vec![Content::text(value.to_string())])
}
_ => Err(ToolError::NotFound(format!("Tool {} not found", tool_name))),
_ => Err(ErrorData {
code: ErrorCode::INVALID_REQUEST,
message: Cow::from(format!("Tool {} not found", tool_name)),
data: None,
}),
}
})
}
+5 -4
View File
@@ -6,7 +6,7 @@ use std::{
type PromptFuture = Pin<Box<dyn Future<Output = Result<String, PromptError>> + Send + 'static>>;
use mcp_core::{
handler::{PromptError, ResourceError, ToolError},
handler::{PromptError, ResourceError},
protocol::{
CallToolResult, Implementation, InitializeResult, ListPromptsResult, ListResourcesResult,
ListToolsResult, PromptsCapability, ReadResourceResult, ResourcesCapability,
@@ -14,8 +14,9 @@ use mcp_core::{
},
};
use rmcp::model::{
Content, GetPromptResult, JsonRpcMessage, JsonRpcRequest, JsonRpcResponse, JsonRpcVersion2_0,
Prompt, PromptMessage, PromptMessageRole, RequestId, Resource, ResourceContents,
Content, ErrorData, GetPromptResult, JsonRpcMessage, JsonRpcRequest, JsonRpcResponse,
JsonRpcVersion2_0, Prompt, PromptMessage, PromptMessageRole, RequestId, Resource,
ResourceContents,
};
use serde_json::Value;
use tokio::sync::mpsc;
@@ -92,7 +93,7 @@ pub trait Router: Send + Sync + 'static {
tool_name: &str,
arguments: Value,
notifier: mpsc::Sender<JsonRpcMessage>,
) -> Pin<Box<dyn Future<Output = Result<Vec<Content>, ToolError>> + Send + 'static>>;
) -> Pin<Box<dyn Future<Output = Result<Vec<Content>, ErrorData>> + Send + 'static>>;
fn list_resources(&self) -> Vec<Resource>;
fn read_resource(
&self,
@@ -48,7 +48,7 @@ async fn echo(&self, params: Value) -> AgentResult<Value>
### Error Handling
The system uses two main error types:
- `ToolError`: Specific errors related to tool execution
- `ErrorData`: Specific errors related to tool execution
- `anyhow::Error`: General purpose errors for extension status and other operations
This split allows precise error handling for tool execution while maintaining flexibility for general extension operations.
@@ -65,7 +65,7 @@ This split allows precise error handling for tool execution while maintaining fl
### Extension Implementation
1. **State Encapsulation**: Keep extension state private and controlled
2. **Error Propagation**: Use `?` operator with `ToolError` for tool execution
2. **Error Propagation**: Use `?` operator with `ErrorData` for tool execution
3. **Status Clarity**: Provide clear, structured status information
4. **Documentation**: Document all tools and their effects
@@ -90,7 +90,11 @@ impl FileSystem {
let full_path = self.root_path.join(path);
let content = tokio::fs::read_to_string(full_path)
.await
.map_err(|e| ToolError::ExecutionError(e.to_string()))?;
.map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(e.to_string(),
data: None,
}))?;
Ok(json!({ "content": content }))
}