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