Skip to main content

systemprompt_cli/commands/plugins/mcp/
call_client.rs

1//! MCP client for `plugins mcp call`.
2//!
3//! Copyright (c) systemprompt.io — Business Source License 1.1.
4//! See <https://systemprompt.io> for licensing details.
5
6use 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}