388 lines
12 KiB
Rust
388 lines
12 KiB
Rust
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
|
|
}
|
|
}
|