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