alexhancock/mcp-crate-cleanup (#4885)
This commit is contained in:
@@ -34,6 +34,7 @@ use crate::agents::types::{FrontendTool, ToolResultReceiver};
|
||||
use crate::config::{Config, ExtensionConfigManager};
|
||||
use crate::context_mgmt::auto_compact;
|
||||
use crate::conversation::{debug_conversation_fix, fix_conversation, Conversation};
|
||||
use crate::mcp_utils::ToolResult;
|
||||
use crate::permission::permission_inspector::PermissionInspector;
|
||||
use crate::permission::permission_judge::PermissionCheckResult;
|
||||
use crate::permission::PermissionConfirmation;
|
||||
@@ -45,10 +46,10 @@ use crate::security::security_inspector::SecurityInspector;
|
||||
use crate::tool_inspection::ToolInspectionManager;
|
||||
use crate::tool_monitor::RepetitionInspector;
|
||||
use crate::utils::is_token_cancelled;
|
||||
use mcp_core::ToolResult;
|
||||
use regex::Regex;
|
||||
use rmcp::model::{
|
||||
Content, ErrorCode, ErrorData, GetPromptResult, Prompt, ServerNotification, Tool,
|
||||
CallToolRequestParam, Content, ErrorCode, ErrorData, GetPromptResult, Prompt,
|
||||
ServerNotification, Tool,
|
||||
};
|
||||
use serde_json::Value;
|
||||
use tokio::sync::{mpsc, Mutex};
|
||||
@@ -389,14 +390,18 @@ impl Agent {
|
||||
#[instrument(skip(self, tool_call, request_id), fields(input, output))]
|
||||
pub async fn dispatch_tool_call(
|
||||
&self,
|
||||
tool_call: mcp_core::tool::ToolCall,
|
||||
tool_call: CallToolRequestParam,
|
||||
request_id: String,
|
||||
cancellation_token: Option<CancellationToken>,
|
||||
session: &Option<SessionConfig>,
|
||||
) -> (String, Result<ToolCallResult, ErrorData>) {
|
||||
if tool_call.name == PLATFORM_MANAGE_SCHEDULE_TOOL_NAME {
|
||||
let arguments = tool_call
|
||||
.arguments
|
||||
.map(Value::Object)
|
||||
.unwrap_or(Value::Object(serde_json::Map::new()));
|
||||
let result = self
|
||||
.handle_schedule_management(tool_call.arguments, request_id.clone())
|
||||
.handle_schedule_management(arguments, request_id.clone())
|
||||
.await;
|
||||
return (request_id, Ok(ToolCallResult::from(result)));
|
||||
}
|
||||
@@ -404,13 +409,15 @@ impl Agent {
|
||||
if tool_call.name == PLATFORM_MANAGE_EXTENSIONS_TOOL_NAME {
|
||||
let extension_name = tool_call
|
||||
.arguments
|
||||
.get("extension_name")
|
||||
.as_ref()
|
||||
.and_then(|args| args.get("extension_name"))
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
let action = tool_call
|
||||
.arguments
|
||||
.get("action")
|
||||
.as_ref()
|
||||
.and_then(|args| args.get("action"))
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
@@ -445,19 +452,25 @@ impl Agent {
|
||||
.is_sub_recipe_tool(&tool_call.name)
|
||||
{
|
||||
let sub_recipe_manager = self.sub_recipe_manager.lock().await;
|
||||
let arguments = tool_call
|
||||
.arguments
|
||||
.clone()
|
||||
.map(Value::Object)
|
||||
.unwrap_or(Value::Object(serde_json::Map::new()));
|
||||
sub_recipe_manager
|
||||
.dispatch_sub_recipe_tool_call(
|
||||
&tool_call.name,
|
||||
tool_call.arguments.clone(),
|
||||
&self.tasks_manager,
|
||||
)
|
||||
.dispatch_sub_recipe_tool_call(&tool_call.name, arguments, &self.tasks_manager)
|
||||
.await
|
||||
} else if tool_call.name == SUBAGENT_EXECUTE_TASK_TOOL_NAME {
|
||||
let provider = self.provider().await.ok();
|
||||
let arguments = tool_call
|
||||
.arguments
|
||||
.clone()
|
||||
.map(Value::Object)
|
||||
.unwrap_or(Value::Object(serde_json::Map::new()));
|
||||
|
||||
let task_config = TaskConfig::new(provider);
|
||||
subagent_execute_task_tool::run_tasks(
|
||||
tool_call.arguments.clone(),
|
||||
arguments,
|
||||
task_config,
|
||||
&self.tasks_manager,
|
||||
cancellation_token,
|
||||
@@ -470,29 +483,33 @@ impl Agent {
|
||||
.list_extensions()
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
create_dynamic_task(
|
||||
tool_call.arguments.clone(),
|
||||
&self.tasks_manager,
|
||||
loaded_extensions,
|
||||
)
|
||||
.await
|
||||
let arguments = tool_call
|
||||
.arguments
|
||||
.clone()
|
||||
.map(Value::Object)
|
||||
.unwrap_or(Value::Object(serde_json::Map::new()));
|
||||
create_dynamic_task(arguments, &self.tasks_manager, loaded_extensions).await
|
||||
} else if tool_call.name == PLATFORM_READ_RESOURCE_TOOL_NAME {
|
||||
// Check if the tool is read_resource and handle it separately
|
||||
let arguments = tool_call
|
||||
.arguments
|
||||
.clone()
|
||||
.map(Value::Object)
|
||||
.unwrap_or(Value::Object(serde_json::Map::new()));
|
||||
ToolCallResult::from(
|
||||
self.extension_manager
|
||||
.read_resource(
|
||||
tool_call.arguments.clone(),
|
||||
cancellation_token.unwrap_or_default(),
|
||||
)
|
||||
.read_resource(arguments, cancellation_token.unwrap_or_default())
|
||||
.await,
|
||||
)
|
||||
} else if tool_call.name == PLATFORM_LIST_RESOURCES_TOOL_NAME {
|
||||
let arguments = tool_call
|
||||
.arguments
|
||||
.clone()
|
||||
.map(Value::Object)
|
||||
.unwrap_or(Value::Object(serde_json::Map::new()));
|
||||
ToolCallResult::from(
|
||||
self.extension_manager
|
||||
.list_resources(
|
||||
tool_call.arguments.clone(),
|
||||
cancellation_token.unwrap_or_default(),
|
||||
)
|
||||
.list_resources(arguments, cancellation_token.unwrap_or_default())
|
||||
.await,
|
||||
)
|
||||
} else if tool_call.name == PLATFORM_SEARCH_AVAILABLE_EXTENSIONS_TOOL_NAME {
|
||||
@@ -522,12 +539,14 @@ impl Agent {
|
||||
ToolCallResult::from(Ok(vec![Content::text(todo_content)]))
|
||||
} else if tool_call.name == TODO_WRITE_TOOL_NAME {
|
||||
// Handle task planner write tool
|
||||
let content = tool_call
|
||||
.arguments
|
||||
.get("content")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
let content = match tool_call.arguments {
|
||||
Some(args) => args
|
||||
.get("content")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.to_string(),
|
||||
None => "".to_string(),
|
||||
};
|
||||
|
||||
// Character limit validation
|
||||
let char_count = content.chars().count();
|
||||
@@ -592,7 +611,7 @@ impl Agent {
|
||||
} else if tool_call.name == ROUTER_LLM_SEARCH_TOOL_NAME {
|
||||
match self
|
||||
.tool_route_manager
|
||||
.dispatch_route_search_tool(tool_call.arguments)
|
||||
.dispatch_route_search_tool(tool_call.arguments.unwrap_or_default())
|
||||
.await
|
||||
{
|
||||
Ok(tool_result) => tool_result,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use mcp_client::client::Error as ClientError;
|
||||
use rmcp::model::Tool;
|
||||
use rmcp::service::ClientInitializeError;
|
||||
use rmcp::ServiceError as ClientError;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
use tracing::warn;
|
||||
|
||||
@@ -3,8 +3,6 @@ use axum::http::{HeaderMap, HeaderName};
|
||||
use chrono::{DateTime, Utc};
|
||||
use futures::stream::{FuturesUnordered, StreamExt};
|
||||
use futures::{future, FutureExt};
|
||||
use mcp_core::handler::require_str_parameter;
|
||||
use mcp_core::ToolCall;
|
||||
use rmcp::service::ClientInitializeError;
|
||||
use rmcp::transport::streamable_http_client::StreamableHttpClientTransportConfig;
|
||||
use rmcp::transport::{
|
||||
@@ -27,12 +25,13 @@ use super::extension::{ExtensionConfig, ExtensionError, ExtensionInfo, Extension
|
||||
use super::tool_execution::ToolCallResult;
|
||||
use crate::agents::extension::{Envs, ProcessExit};
|
||||
use crate::agents::extension_malware_check;
|
||||
use crate::agents::mcp_client::{McpClient, McpClientTrait};
|
||||
use crate::config::{Config, ExtensionConfigManager};
|
||||
use crate::oauth::oauth_flow;
|
||||
use crate::prompt_template;
|
||||
use mcp_client::client::{McpClient, McpClientTrait};
|
||||
use rmcp::model::{
|
||||
Content, ErrorCode, ErrorData, GetPromptResult, Prompt, ResourceContents, ServerInfo, Tool,
|
||||
CallToolRequestParam, Content, ErrorCode, ErrorData, GetPromptResult, Prompt, ResourceContents,
|
||||
ServerInfo, Tool,
|
||||
};
|
||||
use rmcp::transport::auth::AuthClient;
|
||||
use serde_json::Value;
|
||||
@@ -135,6 +134,24 @@ fn normalize(input: String) -> String {
|
||||
result.to_lowercase()
|
||||
}
|
||||
|
||||
fn require_str_parameter<'a>(v: &'a serde_json::Value, name: &str) -> 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(ErrorData::new(
|
||||
ErrorCode::INVALID_PARAMS,
|
||||
format!("The parameter {name} must be a string"),
|
||||
None,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_parameter_names(tool: &Tool) -> Vec<String> {
|
||||
tool.input_schema
|
||||
.get("properties")
|
||||
@@ -604,6 +621,7 @@ impl ExtensionManager {
|
||||
cancellation_token: CancellationToken,
|
||||
) -> Result<Vec<Content>, ErrorData> {
|
||||
let uri = require_str_parameter(¶ms, "uri")?;
|
||||
|
||||
let extension_name = params.get("extension_name").and_then(|v| v.as_str());
|
||||
|
||||
// If extension name is provided, we can just look it up
|
||||
@@ -805,7 +823,7 @@ impl ExtensionManager {
|
||||
|
||||
pub async fn dispatch_tool_call(
|
||||
&self,
|
||||
tool_call: ToolCall,
|
||||
tool_call: CallToolRequestParam,
|
||||
cancellation_token: CancellationToken,
|
||||
) -> Result<ToolCallResult> {
|
||||
// Dispatch tool call based on the prefix naming convention
|
||||
@@ -1042,10 +1060,9 @@ impl ExtensionManager {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use mcp_client::client::Error;
|
||||
use mcp_client::client::McpClientTrait;
|
||||
use rmcp::model::CallToolResult;
|
||||
use rmcp::model::InitializeResult;
|
||||
use rmcp::model::{InitializeResult, JsonObject};
|
||||
use rmcp::{object, ServiceError as Error};
|
||||
|
||||
use rmcp::model::ListPromptsResult;
|
||||
use rmcp::model::ListResourcesResult;
|
||||
@@ -1146,7 +1163,7 @@ mod tests {
|
||||
async fn call_tool(
|
||||
&self,
|
||||
name: &str,
|
||||
_arguments: Value,
|
||||
_arguments: Option<JsonObject>,
|
||||
_cancellation_token: CancellationToken,
|
||||
) -> Result<CallToolResult, Error> {
|
||||
match name {
|
||||
@@ -1269,9 +1286,9 @@ mod tests {
|
||||
.await;
|
||||
|
||||
// verify a normal tool call
|
||||
let tool_call = ToolCall {
|
||||
name: "test_client__tool".to_string(),
|
||||
arguments: json!({}),
|
||||
let tool_call = CallToolRequestParam {
|
||||
name: "test_client__tool".to_string().into(),
|
||||
arguments: Some(object!({})),
|
||||
};
|
||||
|
||||
let result = extension_manager
|
||||
@@ -1279,9 +1296,9 @@ mod tests {
|
||||
.await;
|
||||
assert!(result.is_ok());
|
||||
|
||||
let tool_call = ToolCall {
|
||||
name: "test_client__test__tool".to_string(),
|
||||
arguments: json!({}),
|
||||
let tool_call = CallToolRequestParam {
|
||||
name: "test_client__test__tool".to_string().into(),
|
||||
arguments: Some(object!({})),
|
||||
};
|
||||
|
||||
let result = extension_manager
|
||||
@@ -1290,9 +1307,9 @@ mod tests {
|
||||
assert!(result.is_ok());
|
||||
|
||||
// verify a multiple underscores dispatch
|
||||
let tool_call = ToolCall {
|
||||
name: "__cli__ent____tool".to_string(),
|
||||
arguments: json!({}),
|
||||
let tool_call = CallToolRequestParam {
|
||||
name: "__cli__ent____tool".to_string().into(),
|
||||
arguments: Some(object!({})),
|
||||
};
|
||||
|
||||
let result = extension_manager
|
||||
@@ -1301,9 +1318,9 @@ mod tests {
|
||||
assert!(result.is_ok());
|
||||
|
||||
// Test unicode in tool name, "client 🚀" should become "client_"
|
||||
let tool_call = ToolCall {
|
||||
name: "client___tool".to_string(),
|
||||
arguments: json!({}),
|
||||
let tool_call = CallToolRequestParam {
|
||||
name: "client___tool".to_string().into(),
|
||||
arguments: Some(object!({})),
|
||||
};
|
||||
|
||||
let result = extension_manager
|
||||
@@ -1311,9 +1328,9 @@ mod tests {
|
||||
.await;
|
||||
assert!(result.is_ok());
|
||||
|
||||
let tool_call = ToolCall {
|
||||
name: "client___test__tool".to_string(),
|
||||
arguments: json!({}),
|
||||
let tool_call = CallToolRequestParam {
|
||||
name: "client___test__tool".to_string().into(),
|
||||
arguments: Some(object!({})),
|
||||
};
|
||||
|
||||
let result = extension_manager
|
||||
@@ -1322,9 +1339,9 @@ mod tests {
|
||||
assert!(result.is_ok());
|
||||
|
||||
// this should error out, specifically for an ToolError::ExecutionError
|
||||
let invalid_tool_call = ToolCall {
|
||||
name: "client___tools".to_string(),
|
||||
arguments: json!({}),
|
||||
let invalid_tool_call = CallToolRequestParam {
|
||||
name: "client___tools".to_string().into(),
|
||||
arguments: Some(object!({})),
|
||||
};
|
||||
|
||||
let result = extension_manager
|
||||
@@ -1343,9 +1360,9 @@ mod tests {
|
||||
|
||||
// this should error out, specifically with an ToolError::NotFound
|
||||
// this client doesn't exist
|
||||
let invalid_tool_call = ToolCall {
|
||||
name: "_client__tools".to_string(),
|
||||
arguments: json!({}),
|
||||
let invalid_tool_call = CallToolRequestParam {
|
||||
name: "_client__tools".to_string().into(),
|
||||
arguments: Some(object!({})),
|
||||
};
|
||||
|
||||
let result = extension_manager
|
||||
@@ -1427,9 +1444,9 @@ mod tests {
|
||||
.await;
|
||||
|
||||
// Try to call an unavailable tool
|
||||
let unavailable_tool_call = ToolCall {
|
||||
name: "test_extension__tool".to_string(),
|
||||
arguments: json!({}),
|
||||
let unavailable_tool_call = CallToolRequestParam {
|
||||
name: "test_extension__tool".to_string().into(),
|
||||
arguments: Some(object!({})),
|
||||
};
|
||||
|
||||
let result = extension_manager
|
||||
@@ -1446,9 +1463,9 @@ mod tests {
|
||||
}
|
||||
|
||||
// Try to call an available tool - should succeed
|
||||
let available_tool_call = ToolCall {
|
||||
name: "test_extension__available_tool".to_string(),
|
||||
arguments: json!({}),
|
||||
let available_tool_call = CallToolRequestParam {
|
||||
name: "test_extension__available_tool".to_string().into(),
|
||||
arguments: Some(object!({})),
|
||||
};
|
||||
|
||||
let result = extension_manager
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
use crate::agents::tool_execution::ToolCallResult;
|
||||
use crate::recipe::Response;
|
||||
use indoc::formatdoc;
|
||||
use mcp_core::ToolCall;
|
||||
use rmcp::model::{Content, ErrorCode, ErrorData, Tool, ToolAnnotations};
|
||||
use rmcp::model::{CallToolRequestParam, Content, ErrorCode, ErrorData, Tool, ToolAnnotations};
|
||||
use serde_json::Value;
|
||||
use std::borrow::Cow;
|
||||
|
||||
@@ -117,10 +116,10 @@ impl FinalOutputTool {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn execute_tool_call(&mut self, tool_call: ToolCall) -> ToolCallResult {
|
||||
match tool_call.name.as_str() {
|
||||
pub async fn execute_tool_call(&mut self, tool_call: CallToolRequestParam) -> ToolCallResult {
|
||||
match tool_call.name.to_string().as_str() {
|
||||
FINAL_OUTPUT_TOOL_NAME => {
|
||||
let result = self.validate_json_output(&tool_call.arguments).await;
|
||||
let result = self.validate_json_output(&tool_call.arguments.into()).await;
|
||||
match result {
|
||||
Ok(parsed_value) => {
|
||||
self.final_output = Some(Self::parsed_final_output_string(parsed_value));
|
||||
@@ -153,6 +152,8 @@ impl FinalOutputTool {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::recipe::Response;
|
||||
use rmcp::model::CallToolRequestParam;
|
||||
use rmcp::object;
|
||||
use serde_json::json;
|
||||
|
||||
fn create_complex_test_schema() -> Value {
|
||||
@@ -226,11 +227,11 @@ mod tests {
|
||||
};
|
||||
|
||||
let mut tool = FinalOutputTool::new(response);
|
||||
let tool_call = ToolCall {
|
||||
name: FINAL_OUTPUT_TOOL_NAME.to_string(),
|
||||
arguments: json!({
|
||||
let tool_call = CallToolRequestParam {
|
||||
name: FINAL_OUTPUT_TOOL_NAME.into(),
|
||||
arguments: Some(object!({
|
||||
"message": "Hello" // Missing required "count" field
|
||||
}),
|
||||
})),
|
||||
};
|
||||
|
||||
let result = tool.execute_tool_call(tool_call).await;
|
||||
@@ -248,15 +249,15 @@ mod tests {
|
||||
};
|
||||
|
||||
let mut tool = FinalOutputTool::new(response);
|
||||
let tool_call = ToolCall {
|
||||
name: FINAL_OUTPUT_TOOL_NAME.to_string(),
|
||||
arguments: json!({
|
||||
let tool_call = CallToolRequestParam {
|
||||
name: FINAL_OUTPUT_TOOL_NAME.into(),
|
||||
arguments: Some(object!({
|
||||
"user": {
|
||||
"name": "John",
|
||||
"age": 30
|
||||
},
|
||||
"tags": ["developer", "rust"]
|
||||
}),
|
||||
})),
|
||||
};
|
||||
|
||||
let result = tool.execute_tool_call(tool_call).await;
|
||||
|
||||
@@ -0,0 +1,387 @@
|
||||
use rmcp::model::JsonObject;
|
||||
/// MCP client implementation for Goose
|
||||
use rmcp::{
|
||||
model::{
|
||||
CallToolRequest, CallToolRequestParam, CallToolResult, CancelledNotification,
|
||||
CancelledNotificationMethod, CancelledNotificationParam, ClientCapabilities, ClientInfo,
|
||||
ClientRequest, GetPromptRequest, GetPromptRequestParam, GetPromptResult, Implementation,
|
||||
InitializeResult, ListPromptsRequest, ListPromptsResult, ListResourcesRequest,
|
||||
ListResourcesResult, ListToolsRequest, ListToolsResult, LoggingMessageNotification,
|
||||
LoggingMessageNotificationMethod, PaginatedRequestParam, ProgressNotification,
|
||||
ProgressNotificationMethod, ProtocolVersion, ReadResourceRequest, ReadResourceRequestParam,
|
||||
ReadResourceResult, RequestId, ServerNotification, ServerResult,
|
||||
},
|
||||
service::{
|
||||
ClientInitializeError, PeerRequestOptions, RequestHandle, RunningService, ServiceRole,
|
||||
},
|
||||
transport::IntoTransport,
|
||||
ClientHandler, Peer, RoleClient, ServiceError, ServiceExt,
|
||||
};
|
||||
use serde_json::Value;
|
||||
use std::{sync::Arc, time::Duration};
|
||||
use tokio::sync::{
|
||||
mpsc::{self, Sender},
|
||||
Mutex,
|
||||
};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
pub type BoxError = Box<dyn std::error::Error + Sync + Send>;
|
||||
|
||||
pub type Error = rmcp::ServiceError;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait McpClientTrait: Send + Sync {
|
||||
async fn list_resources(
|
||||
&self,
|
||||
next_cursor: Option<String>,
|
||||
cancel_token: CancellationToken,
|
||||
) -> Result<ListResourcesResult, Error>;
|
||||
|
||||
async fn read_resource(
|
||||
&self,
|
||||
uri: &str,
|
||||
cancel_token: CancellationToken,
|
||||
) -> Result<ReadResourceResult, Error>;
|
||||
|
||||
async fn list_tools(
|
||||
&self,
|
||||
next_cursor: Option<String>,
|
||||
cancel_token: CancellationToken,
|
||||
) -> Result<ListToolsResult, Error>;
|
||||
|
||||
async fn call_tool(
|
||||
&self,
|
||||
name: &str,
|
||||
arguments: Option<JsonObject>,
|
||||
cancel_token: CancellationToken,
|
||||
) -> Result<CallToolResult, Error>;
|
||||
|
||||
async fn list_prompts(
|
||||
&self,
|
||||
next_cursor: Option<String>,
|
||||
cancel_token: CancellationToken,
|
||||
) -> Result<ListPromptsResult, Error>;
|
||||
|
||||
async fn get_prompt(
|
||||
&self,
|
||||
name: &str,
|
||||
arguments: Value,
|
||||
cancel_token: CancellationToken,
|
||||
) -> Result<GetPromptResult, Error>;
|
||||
|
||||
async fn subscribe(&self) -> mpsc::Receiver<ServerNotification>;
|
||||
|
||||
fn get_info(&self) -> Option<&InitializeResult>;
|
||||
}
|
||||
|
||||
pub struct GooseClient {
|
||||
notification_handlers: Arc<Mutex<Vec<Sender<ServerNotification>>>>,
|
||||
}
|
||||
|
||||
impl GooseClient {
|
||||
pub fn new(handlers: Arc<Mutex<Vec<Sender<ServerNotification>>>>) -> Self {
|
||||
GooseClient {
|
||||
notification_handlers: handlers,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ClientHandler for GooseClient {
|
||||
async fn on_progress(
|
||||
&self,
|
||||
params: rmcp::model::ProgressNotificationParam,
|
||||
context: rmcp::service::NotificationContext<rmcp::RoleClient>,
|
||||
) {
|
||||
self.notification_handlers
|
||||
.lock()
|
||||
.await
|
||||
.iter()
|
||||
.for_each(|handler| {
|
||||
let _ = handler.try_send(ServerNotification::ProgressNotification(
|
||||
ProgressNotification {
|
||||
params: params.clone(),
|
||||
method: ProgressNotificationMethod,
|
||||
extensions: context.extensions.clone(),
|
||||
},
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
async fn on_logging_message(
|
||||
&self,
|
||||
params: rmcp::model::LoggingMessageNotificationParam,
|
||||
context: rmcp::service::NotificationContext<rmcp::RoleClient>,
|
||||
) {
|
||||
self.notification_handlers
|
||||
.lock()
|
||||
.await
|
||||
.iter()
|
||||
.for_each(|handler| {
|
||||
let _ = handler.try_send(ServerNotification::LoggingMessageNotification(
|
||||
LoggingMessageNotification {
|
||||
params: params.clone(),
|
||||
method: LoggingMessageNotificationMethod,
|
||||
extensions: context.extensions.clone(),
|
||||
},
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
fn get_info(&self) -> ClientInfo {
|
||||
ClientInfo {
|
||||
protocol_version: ProtocolVersion::V_2025_03_26,
|
||||
capabilities: ClientCapabilities::builder().build(),
|
||||
client_info: Implementation {
|
||||
name: "goose".to_string(),
|
||||
version: env!("CARGO_PKG_VERSION").to_owned(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The MCP client is the interface for MCP operations.
|
||||
pub struct McpClient {
|
||||
client: Mutex<RunningService<RoleClient, GooseClient>>,
|
||||
notification_subscribers: Arc<Mutex<Vec<mpsc::Sender<ServerNotification>>>>,
|
||||
server_info: Option<InitializeResult>,
|
||||
timeout: std::time::Duration,
|
||||
}
|
||||
|
||||
impl McpClient {
|
||||
pub async fn connect<T, E, A>(
|
||||
transport: T,
|
||||
timeout: std::time::Duration,
|
||||
) -> Result<Self, ClientInitializeError>
|
||||
where
|
||||
T: IntoTransport<RoleClient, E, A>,
|
||||
E: std::error::Error + From<std::io::Error> + Send + Sync + 'static,
|
||||
{
|
||||
let notification_subscribers =
|
||||
Arc::new(Mutex::new(Vec::<mpsc::Sender<ServerNotification>>::new()));
|
||||
|
||||
let client = GooseClient::new(notification_subscribers.clone());
|
||||
let client: rmcp::service::RunningService<rmcp::RoleClient, GooseClient> =
|
||||
client.serve(transport).await?;
|
||||
let server_info = client.peer_info().cloned();
|
||||
|
||||
Ok(Self {
|
||||
client: Mutex::new(client),
|
||||
notification_subscribers,
|
||||
server_info,
|
||||
timeout,
|
||||
})
|
||||
}
|
||||
|
||||
async fn send_request(
|
||||
&self,
|
||||
request: ClientRequest,
|
||||
cancel_token: CancellationToken,
|
||||
) -> Result<ServerResult, Error> {
|
||||
let handle = self
|
||||
.client
|
||||
.lock()
|
||||
.await
|
||||
.send_cancellable_request(request, PeerRequestOptions::no_options())
|
||||
.await?;
|
||||
|
||||
await_response(handle, self.timeout, &cancel_token).await
|
||||
}
|
||||
}
|
||||
|
||||
async fn await_response(
|
||||
handle: RequestHandle<RoleClient>,
|
||||
timeout: Duration,
|
||||
cancel_token: &CancellationToken,
|
||||
) -> Result<<RoleClient as ServiceRole>::PeerResp, ServiceError> {
|
||||
let receiver = handle.rx;
|
||||
let peer = handle.peer;
|
||||
let request_id = handle.id;
|
||||
tokio::select! {
|
||||
result = receiver => {
|
||||
result.map_err(|_e| ServiceError::TransportClosed)?
|
||||
}
|
||||
_ = tokio::time::sleep(timeout) => {
|
||||
send_cancel_message(&peer, request_id, Some("timed out".to_owned())).await?;
|
||||
Err(ServiceError::Timeout{timeout})
|
||||
}
|
||||
_ = cancel_token.cancelled() => {
|
||||
send_cancel_message(&peer, request_id, Some("operation cancelled".to_owned())).await?;
|
||||
Err(ServiceError::Cancelled { reason: None })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_cancel_message(
|
||||
peer: &Peer<RoleClient>,
|
||||
request_id: RequestId,
|
||||
reason: Option<String>,
|
||||
) -> Result<(), ServiceError> {
|
||||
peer.send_notification(
|
||||
CancelledNotification {
|
||||
params: CancelledNotificationParam { request_id, reason },
|
||||
method: CancelledNotificationMethod,
|
||||
extensions: Default::default(),
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl McpClientTrait for McpClient {
|
||||
fn get_info(&self) -> Option<&InitializeResult> {
|
||||
self.server_info.as_ref()
|
||||
}
|
||||
|
||||
async fn list_resources(
|
||||
&self,
|
||||
cursor: Option<String>,
|
||||
cancel_token: CancellationToken,
|
||||
) -> Result<ListResourcesResult, Error> {
|
||||
let res = self
|
||||
.send_request(
|
||||
ClientRequest::ListResourcesRequest(ListResourcesRequest {
|
||||
params: Some(PaginatedRequestParam { cursor }),
|
||||
method: Default::default(),
|
||||
extensions: Default::default(),
|
||||
}),
|
||||
cancel_token,
|
||||
)
|
||||
.await?;
|
||||
|
||||
match res {
|
||||
ServerResult::ListResourcesResult(result) => Ok(result),
|
||||
_ => Err(ServiceError::UnexpectedResponse),
|
||||
}
|
||||
}
|
||||
|
||||
async fn read_resource(
|
||||
&self,
|
||||
uri: &str,
|
||||
cancel_token: CancellationToken,
|
||||
) -> Result<ReadResourceResult, Error> {
|
||||
let res = self
|
||||
.send_request(
|
||||
ClientRequest::ReadResourceRequest(ReadResourceRequest {
|
||||
params: ReadResourceRequestParam {
|
||||
uri: uri.to_string(),
|
||||
},
|
||||
method: Default::default(),
|
||||
extensions: Default::default(),
|
||||
}),
|
||||
cancel_token,
|
||||
)
|
||||
.await?;
|
||||
|
||||
match res {
|
||||
ServerResult::ReadResourceResult(result) => Ok(result),
|
||||
_ => Err(ServiceError::UnexpectedResponse),
|
||||
}
|
||||
}
|
||||
|
||||
async fn list_tools(
|
||||
&self,
|
||||
cursor: Option<String>,
|
||||
cancel_token: CancellationToken,
|
||||
) -> Result<ListToolsResult, Error> {
|
||||
let res = self
|
||||
.send_request(
|
||||
ClientRequest::ListToolsRequest(ListToolsRequest {
|
||||
params: Some(PaginatedRequestParam { cursor }),
|
||||
method: Default::default(),
|
||||
extensions: Default::default(),
|
||||
}),
|
||||
cancel_token,
|
||||
)
|
||||
.await?;
|
||||
|
||||
match res {
|
||||
ServerResult::ListToolsResult(result) => Ok(result),
|
||||
_ => Err(ServiceError::UnexpectedResponse),
|
||||
}
|
||||
}
|
||||
|
||||
async fn call_tool(
|
||||
&self,
|
||||
name: &str,
|
||||
arguments: Option<JsonObject>,
|
||||
cancel_token: CancellationToken,
|
||||
) -> Result<CallToolResult, Error> {
|
||||
let res = self
|
||||
.send_request(
|
||||
ClientRequest::CallToolRequest(CallToolRequest {
|
||||
params: CallToolRequestParam {
|
||||
name: name.to_string().into(),
|
||||
arguments,
|
||||
},
|
||||
method: Default::default(),
|
||||
extensions: Default::default(),
|
||||
}),
|
||||
cancel_token,
|
||||
)
|
||||
.await?;
|
||||
|
||||
match res {
|
||||
ServerResult::CallToolResult(result) => Ok(result),
|
||||
_ => Err(ServiceError::UnexpectedResponse),
|
||||
}
|
||||
}
|
||||
|
||||
async fn list_prompts(
|
||||
&self,
|
||||
cursor: Option<String>,
|
||||
cancel_token: CancellationToken,
|
||||
) -> Result<ListPromptsResult, Error> {
|
||||
let res = self
|
||||
.send_request(
|
||||
ClientRequest::ListPromptsRequest(ListPromptsRequest {
|
||||
params: Some(PaginatedRequestParam { cursor }),
|
||||
method: Default::default(),
|
||||
extensions: Default::default(),
|
||||
}),
|
||||
cancel_token,
|
||||
)
|
||||
.await?;
|
||||
|
||||
match res {
|
||||
ServerResult::ListPromptsResult(result) => Ok(result),
|
||||
_ => Err(ServiceError::UnexpectedResponse),
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_prompt(
|
||||
&self,
|
||||
name: &str,
|
||||
arguments: Value,
|
||||
cancel_token: CancellationToken,
|
||||
) -> Result<GetPromptResult, Error> {
|
||||
let arguments = match arguments {
|
||||
Value::Object(map) => Some(map),
|
||||
_ => None,
|
||||
};
|
||||
let res = self
|
||||
.send_request(
|
||||
ClientRequest::GetPromptRequest(GetPromptRequest {
|
||||
params: GetPromptRequestParam {
|
||||
name: name.to_string(),
|
||||
arguments,
|
||||
},
|
||||
method: Default::default(),
|
||||
extensions: Default::default(),
|
||||
}),
|
||||
cancel_token,
|
||||
)
|
||||
.await?;
|
||||
|
||||
match res {
|
||||
ServerResult::GetPromptResult(result) => Ok(result),
|
||||
_ => Err(ServiceError::UnexpectedResponse),
|
||||
}
|
||||
}
|
||||
|
||||
async fn subscribe(&self) -> mpsc::Receiver<ServerNotification> {
|
||||
let (tx, rx) = mpsc::channel(16);
|
||||
self.notification_subscribers.lock().await.push(tx);
|
||||
rx
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ pub mod extension_malware_check;
|
||||
pub mod extension_manager;
|
||||
pub mod final_output_tool;
|
||||
mod large_response_handler;
|
||||
pub mod mcp_client;
|
||||
pub mod model_selector;
|
||||
pub mod platform_tools;
|
||||
pub mod prompt_manager;
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
use rmcp::model::Tool;
|
||||
use rmcp::model::{Content, ErrorCode, ErrorData};
|
||||
use rmcp::model::{JsonObject, Tool};
|
||||
|
||||
use anyhow::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;
|
||||
@@ -23,7 +22,7 @@ struct ToolSelectorContext {
|
||||
|
||||
#[async_trait]
|
||||
pub trait RouterToolSelector: Send + Sync {
|
||||
async fn select_tools(&self, params: Value) -> Result<Vec<Content>, ErrorData>;
|
||||
async fn select_tools(&self, params: JsonObject) -> 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>;
|
||||
@@ -48,7 +47,7 @@ impl LLMToolSelector {
|
||||
|
||||
#[async_trait]
|
||||
impl RouterToolSelector for LLMToolSelector {
|
||||
async fn select_tools(&self, params: Value) -> Result<Vec<Content>, ErrorData> {
|
||||
async fn select_tools(&self, params: JsonObject) -> Result<Vec<Content>, ErrorData> {
|
||||
let query = params
|
||||
.get("query")
|
||||
.and_then(|v| v.as_str())
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::mcp_utils::ToolResult;
|
||||
use chrono::Utc;
|
||||
use mcp_core::ToolResult;
|
||||
use rmcp::model::{Content, ErrorCode, ErrorData};
|
||||
|
||||
use crate::recipe::Recipe;
|
||||
|
||||
@@ -8,8 +8,8 @@ use tokio::sync::Mutex;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::config::permission::PermissionLevel;
|
||||
use crate::mcp_utils::ToolResult;
|
||||
use crate::permission::Permission;
|
||||
use mcp_core::ToolResult;
|
||||
use rmcp::model::{Content, ServerNotification};
|
||||
|
||||
// ToolCallResult combines the result of a tool call with an optional notification stream that
|
||||
@@ -70,8 +70,8 @@ impl Agent {
|
||||
|
||||
let confirmation = Message::user().with_tool_confirmation_request(
|
||||
request.id.clone(),
|
||||
tool_call.name.clone(),
|
||||
tool_call.arguments.clone(),
|
||||
tool_call.name.to_string().clone(),
|
||||
tool_call.arguments.clone().unwrap_or_default(),
|
||||
security_message,
|
||||
);
|
||||
yield confirmation;
|
||||
@@ -80,6 +80,7 @@ impl Agent {
|
||||
while let Some((req_id, confirmation)) = rx.recv().await {
|
||||
if req_id == request.id {
|
||||
if confirmation.permission == Permission::AllowOnce || confirmation.permission == Permission::AlwaysAllow {
|
||||
// Clone tool_call to avoid moving it
|
||||
let (req_id, tool_result) = self.dispatch_tool_call(tool_call.clone(), request.id.clone(), cancellation_token.clone(), &None).await;
|
||||
let mut futures = tool_futures.lock().await;
|
||||
|
||||
|
||||
@@ -7,8 +7,7 @@ use crate::config::Config;
|
||||
use crate::conversation::message::ToolRequest;
|
||||
use crate::providers::base::Provider;
|
||||
use anyhow::{anyhow, Result};
|
||||
use rmcp::model::{ErrorCode, ErrorData, Tool};
|
||||
use serde_json::Value;
|
||||
use rmcp::model::{ErrorCode, ErrorData, JsonObject, Tool};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
use tracing::error;
|
||||
@@ -46,7 +45,7 @@ impl ToolRouteManager {
|
||||
|
||||
pub async fn dispatch_route_search_tool(
|
||||
&self,
|
||||
arguments: Value,
|
||||
arguments: JsonObject,
|
||||
) -> Result<ToolCallResult, ErrorData> {
|
||||
let selector = self.router_tool_selector.lock().await.clone();
|
||||
match selector.as_ref() {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use mcp_core::ToolResult;
|
||||
use crate::mcp_utils::ToolResult;
|
||||
use rmcp::model::{Content, Tool};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
|
||||
Reference in New Issue
Block a user