systemprompt_cli/commands/shared/
mcp_tools.rs1use anyhow::{Context, Result};
7use rmcp::ServiceExt;
8use rmcp::model::{ClientCapabilities, ClientInfo, Implementation};
9use rmcp::transport::streamable_http_client::{
10 StreamableHttpClientTransport, StreamableHttpClientTransportConfig,
11};
12use std::time::Duration;
13use systemprompt_identifiers::{AgentName, ContextId, SessionId, SessionToken, TraceId};
14use systemprompt_mcp::McpServerConfig;
15use systemprompt_mcp::services::SpawnTarget;
16use systemprompt_mcp::services::client::HttpClientWithContext;
17use systemprompt_models::execution::context::RequestContext;
18use tokio::time::timeout;
19use tracing::debug;
20
21fn probe_context(server_name: &str) -> RequestContext {
22 RequestContext::new(
23 SessionId::new(format!("cli-{server_name}")),
24 TraceId::generate(),
25 ContextId::derived_from_cli_probe(server_name),
26 AgentName::system(),
27 )
28}
29
30#[derive(Debug)]
31pub struct ToolInfo {
32 pub name: String,
33 pub description: Option<String>,
34 pub parameters_count: usize,
35 pub input_schema: Option<serde_json::Value>,
36 pub output_schema: Option<serde_json::Value>,
37}
38
39pub fn direct_url(server: &McpServerConfig) -> Result<String> {
42 if server.is_external() {
43 return Ok(server.remote_endpoint.clone());
44 }
45 Ok(format!("http://127.0.0.1:{}/mcp", server.spawn_port()?))
46}
47
48pub async fn list_tools_unauthenticated(
49 server_name: &str,
50 url: &str,
51 timeout_secs: u64,
52) -> Result<Vec<ToolInfo>> {
53 let config = StreamableHttpClientTransportConfig::with_uri(url);
54 let transport = StreamableHttpClientTransport::with_client(
55 HttpClientWithContext::new(probe_context(server_name))?,
56 config,
57 );
58
59 let client_info = ClientInfo::new(
60 ClientCapabilities::default(),
61 Implementation::new(format!("systemprompt-cli-{}", server_name), "1.0.0"),
62 );
63
64 let client = timeout(
65 Duration::from_secs(timeout_secs),
66 client_info.serve(transport),
67 )
68 .await
69 .context("Connection timeout")?
70 .context("Failed to connect to MCP server")?;
71
72 let tools_response = client
73 .list_tools(None)
74 .await
75 .context("Failed to list tools")?;
76
77 let tools: Vec<ToolInfo> = tools_response
78 .tools
79 .into_iter()
80 .map(convert_tool_info)
81 .collect();
82
83 client.cancel().await?;
84 Ok(tools)
85}
86
87pub async fn list_tools_authenticated(
88 server_name: &str,
89 url: &str,
90 token: &SessionToken,
91 timeout_secs: u64,
92) -> Result<Vec<ToolInfo>> {
93 let config =
94 StreamableHttpClientTransportConfig::with_uri(url).auth_header(token.as_str().to_owned());
95 let context = probe_context(server_name).with_auth_token(token.as_str());
96 let transport =
97 StreamableHttpClientTransport::with_client(HttpClientWithContext::new(context)?, config);
98
99 let client_info = ClientInfo::new(
100 ClientCapabilities::default(),
101 Implementation::new(format!("systemprompt-cli-{}", server_name), "1.0.0"),
102 );
103
104 let client = timeout(
105 Duration::from_secs(timeout_secs),
106 client_info.serve(transport),
107 )
108 .await
109 .context("Connection timeout")?
110 .context("Failed to connect to MCP server")?;
111
112 let tools_response = client
113 .list_tools(None)
114 .await
115 .context("Failed to list tools")?;
116
117 let tools: Vec<ToolInfo> = tools_response
118 .tools
119 .into_iter()
120 .map(convert_tool_info)
121 .collect();
122
123 client.cancel().await?;
124 Ok(tools)
125}
126
127fn convert_tool_info(tool: rmcp::model::Tool) -> ToolInfo {
128 let input_schema = match serde_json::to_value(&tool.input_schema) {
129 Ok(schema) => Some(schema),
130 Err(e) => {
131 debug!(tool = %tool.name, error = %e, "Failed to serialize input schema");
132 None
133 },
134 };
135 let output_schema =
136 tool.output_schema
137 .as_ref()
138 .and_then(|s| match serde_json::to_value(s.as_ref()) {
139 Ok(schema) => Some(schema),
140 Err(e) => {
141 debug!(tool = %tool.name, error = %e, "Failed to serialize output schema");
142 None
143 },
144 });
145 let parameters_count = input_schema
146 .as_ref()
147 .and_then(|s| s.get("properties"))
148 .and_then(|p| p.as_object())
149 .map_or(0, serde_json::Map::len);
150
151 ToolInfo {
152 name: tool.name.to_string(),
153 description: tool.description.map(|d| d.to_string()),
154 parameters_count,
155 input_schema,
156 output_schema,
157 }
158}