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