systemprompt_cli/commands/plugins/mcp/
call_client.rs1use anyhow::{Context, Result};
7use rmcp::ServiceExt;
8use rmcp::model::{
9 CallToolRequestParams, ClientCapabilities, ClientInfo, ContentBlock, Implementation,
10};
11use rmcp::transport::streamable_http_client::{
12 StreamableHttpClientTransport, StreamableHttpClientTransportConfig,
13};
14use std::time::Duration;
15use systemprompt_identifiers::{AgentName, McpServerId, McpToolName};
16use systemprompt_mcp::services::client::HttpClientWithContext;
17use systemprompt_models::ai::tools::CallToolResult;
18use tokio::time::timeout;
19
20use super::types::McpToolContent;
21use crate::session::CliSessionContext;
22
23#[derive(Debug)]
24pub struct ToolCallParams<'a> {
25 pub server_name: &'a McpServerId,
26 pub url: &'a str,
27 pub tool_name: &'a McpToolName,
28 pub arguments: Option<serde_json::Value>,
30 pub session_ctx: &'a CliSessionContext,
31 pub timeout_secs: u64,
32}
33
34pub async fn execute_tool_call(params: ToolCallParams<'_>) -> Result<CallToolResult> {
35 let ToolCallParams {
36 server_name,
37 url,
38 tool_name,
39 arguments,
40 session_ctx,
41 timeout_secs,
42 } = params;
43
44 let agent_name = AgentName::try_new(format!("cli-{server_name}"))
45 .context("MCP server name does not form a valid agent name")?;
46 let request_context = session_ctx.to_request_context(agent_name);
47 let http_client = HttpClientWithContext::new(request_context)?;
48 let config = StreamableHttpClientTransportConfig::with_uri(url)
49 .auth_header(session_ctx.session_token().as_str().to_owned());
50 let transport = StreamableHttpClientTransport::with_client(http_client, config);
51
52 let client_info = ClientInfo::new(
53 ClientCapabilities::default(),
54 Implementation::new(format!("systemprompt-cli-{}", server_name), "1.0.0"),
55 );
56
57 let client = timeout(
58 Duration::from_secs(timeout_secs),
59 client_info.serve(transport),
60 )
61 .await
62 .context("Connection timeout")?
63 .context("Failed to connect to MCP server")?;
64
65 let mut params = CallToolRequestParams::new(String::from(tool_name));
66 params.arguments = arguments.and_then(|v| v.as_object().cloned());
67
68 let result = client.call_tool(params).await.map_err(|e| {
69 anyhow::anyhow!(
70 "MCP tool '{}' on '{}' rejected the call: {}",
71 tool_name,
72 server_name,
73 e
74 )
75 })?;
76
77 client.cancel().await?;
78 Ok(result)
79}
80
81pub async fn list_available_tools(
82 server_name: &McpServerId,
83 url: &str,
84 session_ctx: &CliSessionContext,
85 timeout_secs: u64,
86) -> Result<Vec<String>> {
87 let agent_name = AgentName::try_new(format!("cli-{server_name}"))
88 .context("MCP server name does not form a valid agent name")?;
89 let request_context = session_ctx.to_request_context(agent_name);
90 let http_client = HttpClientWithContext::new(request_context)?;
91 let config = StreamableHttpClientTransportConfig::with_uri(url)
92 .auth_header(session_ctx.session_token().as_str().to_owned());
93 let transport = StreamableHttpClientTransport::with_client(http_client, config);
94
95 let client_info = ClientInfo::new(
96 ClientCapabilities::default(),
97 Implementation::new(format!("systemprompt-cli-{}", server_name), "1.0.0"),
98 );
99
100 let client = timeout(
101 Duration::from_secs(timeout_secs),
102 client_info.serve(transport),
103 )
104 .await
105 .context("Connection timeout")?
106 .context("Failed to connect to MCP server")?;
107
108 let tools_response = client
109 .list_tools(Some(rmcp::model::PaginatedRequestParams::default()))
112 .await
113 .context("Failed to list tools")?;
114
115 let tool_names: Vec<String> = tools_response
116 .tools
117 .into_iter()
118 .map(|t| t.name.to_string())
119 .collect();
120
121 client.cancel().await?;
122 Ok(tool_names)
123}
124
125pub fn convert_content(content: &ContentBlock) -> McpToolContent {
126 match content {
127 ContentBlock::Text(text) => McpToolContent {
128 kind: "text".to_owned(),
129 text: Some(text.text.clone()),
130 mime_type: None,
131 data: None,
132 },
133 ContentBlock::Image(image) => McpToolContent {
134 kind: "image".to_owned(),
135 text: None,
136 mime_type: Some(image.mime_type.clone()),
137 data: Some(image.data.clone()),
138 },
139 ContentBlock::Resource(resource) => McpToolContent {
140 kind: "resource".to_owned(),
141 text: Some(format!("{:?}", resource.resource)),
142 mime_type: None,
143 data: None,
144 },
145 ContentBlock::Audio(audio) => McpToolContent {
146 kind: "audio".to_owned(),
147 text: None,
148 mime_type: Some(audio.mime_type.clone()),
149 data: Some(audio.data.clone()),
150 },
151 ContentBlock::ResourceLink(link) => McpToolContent {
152 kind: "resource_link".to_owned(),
153 text: Some(link.uri.clone()),
154 mime_type: link.mime_type.clone(),
155 data: None,
156 },
157 _ => McpToolContent {
158 kind: "unknown".to_owned(),
159 text: None,
160 mime_type: None,
161 data: None,
162 },
163 }
164}