mirror of
https://github.com/block/goose.git
synced 2026-07-17 12:56:20 +02:00
feat: ToolError migration to ErrorData (#4051)
This commit is contained in:
Generated
+1
-1
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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(¶ms, "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
@@ -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,
|
||||
)),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(¶ms, "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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(¶ms);
|
||||
|
||||
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;
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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)])
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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 +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,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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 }))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user