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