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(Some(rmcp::model::PaginatedRequestParams::default()))
76 .await
77 .context("Failed to list tools")?;
78
79 let tools: Vec<ToolInfo> = tools_response
80 .tools
81 .into_iter()
82 .map(convert_tool_info)
83 .collect();
84
85 client.cancel().await?;
86 Ok(tools)
87}
88
89pub async fn list_tools_authenticated(
90 server_name: &str,
91 url: &str,
92 token: &SessionToken,
93 timeout_secs: u64,
94) -> Result<Vec<ToolInfo>> {
95 let config =
96 StreamableHttpClientTransportConfig::with_uri(url).auth_header(token.as_str().to_owned());
97 let context = probe_context(server_name).with_auth_token(token.as_str());
98 let transport =
99 StreamableHttpClientTransport::with_client(HttpClientWithContext::new(context)?, config);
100
101 let client_info = ClientInfo::new(
102 ClientCapabilities::default(),
103 Implementation::new(format!("systemprompt-cli-{}", server_name), "1.0.0"),
104 );
105
106 let client = timeout(
107 Duration::from_secs(timeout_secs),
108 client_info.serve(transport),
109 )
110 .await
111 .context("Connection timeout")?
112 .context("Failed to connect to MCP server")?;
113
114 let tools_response = client
115 .list_tools(Some(rmcp::model::PaginatedRequestParams::default()))
118 .await
119 .context("Failed to list tools")?;
120
121 let tools: Vec<ToolInfo> = tools_response
122 .tools
123 .into_iter()
124 .map(convert_tool_info)
125 .collect();
126
127 client.cancel().await?;
128 Ok(tools)
129}
130
131fn convert_tool_info(tool: rmcp::model::Tool) -> ToolInfo {
132 let input_schema = match serde_json::to_value(&tool.input_schema) {
133 Ok(schema) => Some(schema),
134 Err(e) => {
135 debug!(tool = %tool.name, error = %e, "Failed to serialize input schema");
136 None
137 },
138 };
139 let output_schema =
140 tool.output_schema
141 .as_ref()
142 .and_then(|s| match serde_json::to_value(s.as_ref()) {
143 Ok(schema) => Some(schema),
144 Err(e) => {
145 debug!(tool = %tool.name, error = %e, "Failed to serialize output schema");
146 None
147 },
148 });
149 let parameters_count = input_schema
150 .as_ref()
151 .and_then(|s| s.get("properties"))
152 .and_then(|p| p.as_object())
153 .map_or(0, serde_json::Map::len);
154
155 ToolInfo {
156 name: tool.name.to_string(),
157 description: tool.description.map(|d| d.to_string()),
158 parameters_count,
159 input_schema,
160 output_schema,
161 }
162}