1use std::collections::HashMap;
8use std::pin::Pin;
9use std::sync::{Arc, OnceLock};
10
11use rust_mcp_sdk::mcp_client::{
12 client_runtime::create_client, ClientHandler, ClientRuntime, McpClientOptions,
13};
14use rust_mcp_sdk::{McpClient, ToMcpClientHandler};
15use rust_mcp_sdk::{
16 schema::{
17 CallToolRequestParams, CallToolResult, ContentBlock, Implementation, InitializeRequestParams,
18 ListToolsResult, TextContent,
19 },
20 ClientSseTransport, ClientSseTransportOptions, ClientStreamableTransport,
21 RequestOptions, StreamableTransportOptions, StdioTransport,
22};
23
24use crate::services::analytics::log_event;
25use crate::services::mcp::types::*;
26use crate::utils::http::get_user_agent;
27
28#[derive(Debug, Clone)]
37pub struct McpAuthError {
38 pub message: String,
39 pub server_name: String,
40}
41
42impl std::fmt::Display for McpAuthError {
43 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44 write!(f, "McpAuthError({}): {}", self.server_name, self.message)
45 }
46}
47
48impl std::error::Error for McpAuthError {}
49
50impl McpAuthError {
51 pub fn new(server_name: String, message: String) -> Self {
52 Self {
53 server_name,
54 message,
55 }
56 }
57}
58
59#[derive(Debug, Clone)]
62pub struct McpToolCallError {
63 pub message: String,
64 pub telemetry_message: String,
65 pub mcp_meta: Option<McpToolCallMeta>,
66}
67
68#[derive(Debug, Clone, Default, serde::Deserialize)]
69pub struct McpToolCallMeta {
70 #[serde(default)]
71 pub _meta: Option<serde_json::Value>,
72}
73
74impl std::fmt::Display for McpToolCallError {
75 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76 write!(f, "{}", self.message)
77 }
78}
79
80impl std::error::Error for McpToolCallError {}
81
82impl McpToolCallError {
83 pub fn new(
84 message: String,
85 telemetry_message: String,
86 mcp_meta: Option<McpToolCallMeta>,
87 ) -> Self {
88 Self {
89 message,
90 telemetry_message,
91 mcp_meta,
92 }
93 }
94}
95
96#[derive(Debug, Clone)]
103pub struct McpSessionExpiredError {
104 pub server_name: String,
105 pub message: String,
106}
107
108impl std::fmt::Display for McpSessionExpiredError {
109 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110 write!(f, "{}", self.message)
111 }
112}
113
114impl std::error::Error for McpSessionExpiredError {}
115
116impl McpSessionExpiredError {
117 pub fn new(server_name: String) -> Self {
118 Self {
119 server_name: server_name.clone(),
120 message: format!(r#"MCP server "{}" session expired"#, server_name),
121 }
122 }
123}
124
125pub fn is_mcp_session_expired_error(error: &dyn std::error::Error) -> bool {
129 let error_msg = error.to_string();
130
131 if !error_msg.contains("404") {
133 return false;
134 }
135
136 error_msg.contains("\"code\":-32001") || error_msg.contains("\"code\": -32001")
140}
141
142const MCP_AUTH_CACHE_TTL_MS: u64 = 15 * 60 * 1000; type McpAuthCacheData = HashMap<String, McpAuthCacheEntry>;
149
150#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
151struct McpAuthCacheEntry {
152 timestamp: u64,
153}
154
155fn get_mcp_auth_cache_path() -> String {
156 use crate::utils::env_utils::get_claude_config_home_dir;
157 let config_home = get_claude_config_home_dir();
158 format!("{}/mcp-needs-auth-cache.json", config_home)
159}
160
161static AUTH_CACHE: OnceLock<McpAuthCacheData> = OnceLock::new();
164
165fn get_mcp_auth_cache() -> &'static McpAuthCacheData {
166 AUTH_CACHE.get_or_init(|| {
167 let cache_path = get_mcp_auth_cache_path();
168 if let Ok(data) = std::fs::read_to_string(&cache_path) {
169 serde_json::from_str(&data).unwrap_or_default()
170 } else {
171 McpAuthCacheData::new()
172 }
173 })
174}
175
176pub fn is_mcp_auth_cached(server_id: &str) -> bool {
178 let cache = get_mcp_auth_cache();
179 if let Some(entry) = cache.get(server_id) {
180 let now = std::time::SystemTime::now()
181 .duration_since(std::time::UNIX_EPOCH)
182 .map(|d| d.as_millis() as u64)
183 .unwrap_or(0);
184 return now - entry.timestamp < MCP_AUTH_CACHE_TTL_MS;
185 }
186 false
187}
188
189pub fn set_mcp_auth_cache_entry(server_id: &str) {
191 let cache_path = get_mcp_auth_cache_path();
192 let mut cache = get_mcp_auth_cache().clone();
193 let now = std::time::SystemTime::now()
194 .duration_since(std::time::UNIX_EPOCH)
195 .map(|d| d.as_millis() as u64)
196 .unwrap_or(0);
197 cache.insert(server_id.to_string(), McpAuthCacheEntry { timestamp: now });
198
199 if let Ok(json) = serde_json::to_string(&cache) {
201 if let Some(parent) = std::path::Path::new(&cache_path).parent() {
202 let _ = std::fs::create_dir_all(parent);
203 }
204 let _ = std::fs::write(&cache_path, json);
205 }
206}
207
208pub fn clear_mcp_auth_cache() {
210 let cache_path = get_mcp_auth_cache_path();
214 let _ = std::fs::remove_file(cache_path);
215}
216
217const MCP_STREAMABLE_HTTP_ACCEPT: &str = "application/json, text/event-stream";
225
226const MCP_REQUEST_TIMEOUT_MS: u64 = 60000;
228
229pub fn wrap_fetch_with_timeout(
242 _base_fetch: impl Fn(
243 String,
244 ) -> std::pin::Pin<
245 Box<dyn std::future::Future<Output = Result<reqwest::Response, reqwest::Error>> + Send>,
246 > + Send
247 + Sync
248 + 'static,
249) -> impl Fn(
250 String,
251) -> std::pin::Pin<
252 Box<dyn std::future::Future<Output = Result<reqwest::Response, reqwest::Error>> + Send>,
253> + Send
254+ Sync
255+ 'static {
256 move |url: String| {
257 let client = match reqwest::Client::builder()
258 .timeout(std::time::Duration::from_millis(MCP_REQUEST_TIMEOUT_MS))
259 .user_agent(get_user_agent())
260 .build()
261 {
262 Ok(c) => c,
263 Err(e) => {
264 return Box::pin(async { Err(e) })
265 as Pin<
266 Box<
267 dyn std::future::Future<
268 Output = Result<reqwest::Response, reqwest::Error>,
269 > + Send,
270 >,
271 >;
272 }
273 };
274
275 Box::pin(async move {
276 let mut request = client.get(&url);
277 request = request.header("Accept", MCP_STREAMABLE_HTTP_ACCEPT);
278 request.send().await
279 })
280 as Pin<
281 Box<
282 dyn std::future::Future<Output = Result<reqwest::Response, reqwest::Error>>
283 + Send,
284 >,
285 >
286 }
287}
288
289pub fn get_mcp_server_connection_batch_size() -> u32 {
295 std::env::var("MCP_SERVER_CONNECTION_BATCH_SIZE")
296 .ok()
297 .and_then(|v| v.parse().ok())
298 .unwrap_or(3)
299}
300
301fn get_remote_mcp_server_connection_batch_size() -> u32 {
302 std::env::var("MCP_REMOTE_SERVER_CONNECTION_BATCH_SIZE")
303 .ok()
304 .and_then(|v| v.parse().ok())
305 .unwrap_or(20)
306}
307
308pub fn get_server_cache_key(name: &str, server_ref: &ScopedMcpServerConfig) -> String {
317 let config_json = serde_json::to_string(server_ref).unwrap_or_default();
319 format!("{}-{}", name, config_json)
320}
321
322pub fn are_mcp_configs_equal(a: &ScopedMcpServerConfig, b: &ScopedMcpServerConfig) -> bool {
329 if a.config.type_variant() != b.config.type_variant() {
331 return false;
332 }
333
334 let a_json = serde_json::to_string(a).unwrap_or_default();
337 let b_json = serde_json::to_string(b).unwrap_or_default();
338 a_json == b_json
339}
340
341pub fn mcp_tool_input_to_auto_classifier_input(
349 input: &serde_json::Value,
350 tool_name: &str,
351) -> String {
352 if let Some(obj) = input.as_object() {
353 if !obj.is_empty() {
354 return obj
355 .keys()
356 .map(|k| {
357 format!(
358 "{}={}",
359 k,
360 obj.get(k).and_then(|v| v.as_str()).unwrap_or("")
361 )
362 })
363 .collect::<Vec<_>>()
364 .join(" ");
365 }
366 }
367 tool_name.to_string()
368}
369
370pub fn get_mcp_tool_timeout_ms() -> u64 {
376 std::env::var("MCP_TOOL_TIMEOUT")
377 .ok()
378 .and_then(|v| v.parse().ok())
379 .unwrap_or(100_000_000) }
381
382fn get_connection_timeout_ms() -> u32 {
387 std::env::var("MCP_TIMEOUT")
388 .ok()
389 .and_then(|v| v.parse().ok())
390 .unwrap_or(30000)
391}
392
393pub fn is_local_mcp_server(config: &ScopedMcpServerConfig) -> bool {
399 let t = config.config.type_variant();
400 t == "stdio" || t == "sdk" || t.is_empty()
401}
402
403#[derive(Default)]
411pub struct DefaultClientHandler;
412
413#[async_trait::async_trait]
414impl ClientHandler for DefaultClientHandler {}
415
416const MCP_FETCH_CACHE_SIZE: usize = 20;
423
424pub async fn connect_to_server(
427 name: &str,
428 server_ref: &ScopedMcpServerConfig,
429) -> McpServerConnection {
430 let server_type = server_ref.config.type_variant().to_string();
431
432 let result = do_connect_to_server(name, server_ref).await;
433
434 match result {
435 Ok(runtime) => {
436 let server_info = runtime.server_info().map(|info| {
437 let impl_info = info.server_info;
438 McpServerInfo {
439 name: impl_info.name,
440 version: impl_info.version,
441 }
442 });
443 let instructions = runtime.instructions();
444 let capabilities = runtime.server_capabilities().map(|caps| ServerCapabilities {
445 tools: caps.tools.as_ref().map(|_| serde_json::Value::Bool(true)),
446 resources: caps.resources.as_ref().map(|_| serde_json::Value::Bool(true)),
447 prompts: caps.prompts.as_ref().map(|_| serde_json::Value::Bool(true)),
448 logging: caps.logging.as_ref().map(|_| serde_json::Value::Bool(true)),
449 });
450
451 McpServerConnection::Connected(ConnectedMcpServer {
452 name: name.to_string(),
453 server_type,
454 capabilities,
455 server_info,
456 instructions,
457 config: server_ref.clone(),
458 runtime: Some(runtime),
459 })
460 }
461 Err(e) => {
462 log::warn!("[mcp] Failed to connect to server '{}': {}", name, e);
463 McpServerConnection::Failed(FailedMcpServer {
464 name: name.to_string(),
465 server_type,
466 config: server_ref.clone(),
467 error: Some(e.to_string()),
468 })
469 }
470 }
471}
472
473fn build_mcp_headers(
475 headers: &Option<std::collections::HashMap<String, String>>,
476) -> Option<std::collections::HashMap<String, String>> {
477 headers.as_ref().cloned()
478}
479
480async fn do_connect_to_server(
481 name: &str,
482 server_ref: &ScopedMcpServerConfig,
483) -> Result<Arc<ClientRuntime>, String> {
484 let client_details = InitializeRequestParams {
485 capabilities: rust_mcp_sdk::schema::ClientCapabilities::default(),
486 protocol_version: "2024-11-05".to_string(),
487 client_info: Implementation {
488 name: "ai-agent".to_string(),
489 version: env!("CARGO_PKG_VERSION").to_string(),
490 description: None,
491 icons: vec![],
492 title: None,
493 website_url: None,
494 },
495 meta: None,
496 };
497
498 match &server_ref.config {
499 McpServerConfig::Stdio(stdio_config) => {
500 let env_map = stdio_config
501 .env
502 .as_ref()
503 .map(|e| e.iter().map(|(k, v)| (k.clone(), v.clone())).collect());
504 let args = stdio_config.args.clone();
505
506 let transport = StdioTransport::create_with_server_launch(
507 &stdio_config.command,
508 args,
509 env_map,
510 Default::default(),
511 )
512 .map_err(|e| format!("Failed to create stdio transport: {}", e))?;
513
514 let handler = Box::new(DefaultClientHandler).to_mcp_client_handler();
515 let options = rust_mcp_sdk::mcp_client::McpClientOptions {
516 client_details,
517 transport,
518 handler,
519 task_store: None,
520 server_task_store: None,
521 message_observer: None,
522 };
523
524 let runtime = create_client(options);
525 let runtime_clone = runtime.clone();
526 runtime_clone
527 .start()
528 .await
529 .map_err(|e| format!("Failed to start MCP client '{}': {}", name, e))?;
530
531 Ok(runtime)
532 }
533 McpServerConfig::Sse(sse_config) => {
534 let headers = build_mcp_headers(&sse_config.headers);
535 let transport = ClientSseTransport::new(
536 &sse_config.url,
537 ClientSseTransportOptions {
538 request_timeout: std::time::Duration::from_millis(get_connection_timeout_ms() as u64),
539 retry_delay: None,
540 max_retries: None,
541 custom_headers: headers,
542 },
543 )
544 .map_err(|e| format!("Failed to create SSE transport: {}", e))?;
545
546 let handler = Box::new(DefaultClientHandler).to_mcp_client_handler();
547 let options = McpClientOptions {
548 client_details,
549 transport,
550 handler,
551 task_store: None,
552 server_task_store: None,
553 message_observer: None,
554 };
555
556 let runtime = create_client(options);
557 let runtime_clone = runtime.clone();
558 runtime_clone
559 .start()
560 .await
561 .map_err(|e| format!("Failed to start MCP client '{}': {}", name, e))?;
562
563 Ok(runtime)
564 }
565 McpServerConfig::SseIde(ide_config) => {
566 let transport = ClientSseTransport::new(
567 &ide_config.url,
568 ClientSseTransportOptions::default(),
569 )
570 .map_err(|e| format!("Failed to create SSE-IDE transport: {}", e))?;
571
572 let handler = Box::new(DefaultClientHandler).to_mcp_client_handler();
573 let options = McpClientOptions {
574 client_details,
575 transport,
576 handler,
577 task_store: None,
578 server_task_store: None,
579 message_observer: None,
580 };
581
582 let runtime = create_client(options);
583 let runtime_clone = runtime.clone();
584 runtime_clone
585 .start()
586 .await
587 .map_err(|e| format!("Failed to start MCP client '{}': {}", name, e))?;
588
589 Ok(runtime)
590 }
591 McpServerConfig::Http(http_config) => {
592 let headers = build_mcp_headers(&http_config.headers);
593 let transport = ClientStreamableTransport::new(
594 &StreamableTransportOptions {
595 mcp_url: http_config.url.clone(),
596 request_options: RequestOptions {
597 request_timeout: std::time::Duration::from_millis(get_connection_timeout_ms() as u64),
598 retry_delay: None,
599 max_retries: None,
600 custom_headers: headers,
601 },
602 },
603 None,
604 true,
605 )
606 .map_err(|e| format!("Failed to create streamable HTTP transport: {}", e))?;
607
608 let handler = Box::new(DefaultClientHandler).to_mcp_client_handler();
609 let options = McpClientOptions {
610 client_details,
611 transport,
612 handler,
613 task_store: None,
614 server_task_store: None,
615 message_observer: None,
616 };
617
618 let runtime = create_client(options);
619 let runtime_clone = runtime.clone();
620 runtime_clone
621 .start()
622 .await
623 .map_err(|e| format!("Failed to start MCP client '{}': {}", name, e))?;
624
625 Ok(runtime)
626 }
627 McpServerConfig::WebSocket(_) | McpServerConfig::WebSocketIde(_) => {
628 log::warn!(
629 "[mcp] WebSocket transport for '{}' not supported by rust-mcp-sdk",
630 name
631 );
632 Err("WebSocket transport not supported by rust-mcp-sdk".into())
633 }
634 McpServerConfig::Sdk(_) => {
635 log::warn!(
636 "[mcp] SDK (in-process) transport for '{}' requires separate setup path",
637 name
638 );
639 Err("SDK transport requires separate setup path".into())
640 }
641 McpServerConfig::ClaudeAiProxy(proxy_config) => {
642 let transport = ClientStreamableTransport::new(
643 &StreamableTransportOptions {
644 mcp_url: proxy_config.url.clone(),
645 request_options: RequestOptions {
646 request_timeout: std::time::Duration::from_millis(get_connection_timeout_ms() as u64),
647 retry_delay: None,
648 max_retries: None,
649 custom_headers: None,
650 },
651 },
652 None,
653 true,
654 )
655 .map_err(|e| format!("Failed to create Claude.ai proxy transport: {}", e))?;
656
657 let handler = Box::new(DefaultClientHandler).to_mcp_client_handler();
658 let options = McpClientOptions {
659 client_details,
660 transport,
661 handler,
662 task_store: None,
663 server_task_store: None,
664 message_observer: None,
665 };
666
667 let runtime = create_client(options);
668 let runtime_clone = runtime.clone();
669 runtime_clone
670 .start()
671 .await
672 .map_err(|e| format!("Failed to start MCP client '{}': {}", name, e))?;
673
674 Ok(runtime)
675 }
676 }
677}
678
679pub async fn fetch_tools_for_client(client: &McpServerConnection) -> Vec<serde_json::Value> {
682 let McpServerConnection::Connected(server) = client else {
683 return vec![];
684 };
685 let Some(runtime) = &server.runtime else {
686 return vec![];
687 };
688
689 let result = match runtime.request_tool_list(None).await {
690 Ok(r) => r,
691 Err(e) => {
692 log::warn!(
693 "[mcp] Failed to fetch tools from '{}': {}",
694 server.name,
695 e
696 );
697 return vec![];
698 }
699 };
700
701 let tools_result: ListToolsResult = result;
702 tools_result
703 .tools
704 .into_iter()
705 .map(|tool| {
706 serde_json::json!({
707 "name": tool.name,
708 "description": tool.description,
709 "inputSchema": tool.input_schema,
710 "isMcp": true,
711 })
712 })
713 .collect()
714}
715
716pub async fn fetch_resources_for_client(client: &McpServerConnection) -> Vec<ServerResource> {
718 let McpServerConnection::Connected(server) = client else {
719 return vec![];
720 };
721 let Some(runtime) = &server.runtime else {
722 return vec![];
723 };
724
725 let result = match runtime.request_resource_list(None).await {
726 Ok(r) => r,
727 Err(e) => {
728 log::warn!(
729 "[mcp] Failed to fetch resources from '{}': {}",
730 server.name,
731 e
732 );
733 return vec![];
734 }
735 };
736
737 result
738 .resources
739 .into_iter()
740 .map(|r| ServerResource {
741 uri: r.uri,
742 name: Some(r.name),
743 description: r.description,
744 mime_type: r.mime_type,
745 server: server.name.clone(),
746 })
747 .collect()
748}
749
750pub async fn fetch_commands_for_client(
752 client: &McpServerConnection,
753) -> Vec<crate::commands::Command> {
754 let McpServerConnection::Connected(server) = client else {
755 return vec![];
756 };
757 let Some(runtime) = &server.runtime else {
758 return vec![];
759 };
760
761 let result = match runtime.request_prompt_list(None).await {
762 Ok(r) => r,
763 Err(e) => {
764 log::warn!(
765 "[mcp] Failed to fetch prompts from '{}': {}",
766 server.name,
767 e
768 );
769 return vec![];
770 }
771 };
772
773 result
775 .prompts
776 .into_iter()
777 .map(|p| crate::commands::Command {
778 name: p.name,
779 description: p.description.unwrap_or_default(),
780 argument_hint: None,
781 is_hidden: None,
782 supports_non_interactive: None,
783 command_type: "mcp".to_string(),
784 })
785 .collect()
786}
787
788pub async fn call_mcp_tool(
790 client: &McpServerConnection,
791 tool: &str,
792 args: &serde_json::Value,
793) -> Result<TransformedMCPResult, String> {
794 let McpServerConnection::Connected(server) = client else {
795 return Err("MCP server not connected".into());
796 };
797 let Some(runtime) = &server.runtime else {
798 return Err("No runtime available".into());
799 };
800
801 let timeout_ms = get_mcp_tool_timeout_ms();
802 let call_params = CallToolRequestParams {
803 name: tool.to_string(),
804 arguments: Some(
805 args.as_object()
806 .cloned()
807 .unwrap_or_default(),
808 ),
809 meta: None,
810 task: None,
811 };
812
813 let result = tokio::time::timeout(
814 std::time::Duration::from_millis(timeout_ms),
815 runtime.request_tool_call(call_params),
816 )
817 .await
818 .map_err(|_| format!("Tool call '{}' timed out after {}ms", tool, timeout_ms))?
819 .map_err(|e| format!("Tool call '{}' failed: {}", tool, e))?;
820
821 let tool_result: CallToolResult = result;
822
823 if tool_result.is_error == Some(true) {
825 for content in &tool_result.content {
826 if let ContentBlock::TextContent(TextContent { text, .. }) = content {
827 return Err(format!("MCP tool '{}' returned error: {}", tool, text));
828 }
829 }
830 return Err(format!("MCP tool '{}' returned error", tool));
831 }
832
833 let content_json = serde_json::json!({
834 "content": tool_result.content,
835 "meta": tool_result.meta,
836 });
837
838 Ok(TransformedMCPResult {
839 content: content_json,
840 result_type: "toolResult",
841 schema: None,
842 })
843}
844
845pub async fn clear_server_cache(name: &str, config: &ScopedMcpServerConfig) -> Result<(), String> {
848 let _ = config;
852 Ok(())
853}
854
855pub async fn ensure_connected_client(
857 client: McpServerConnection,
858) -> Result<McpServerConnection, String> {
859 match &client {
860 McpServerConnection::Connected(server) => {
861 if let Some(runtime) = &server.runtime {
862 if runtime.is_initialized() {
863 return Ok(client);
864 }
865 if runtime.is_shut_down().await {
867 return Err(format!(
868 "MCP server \"{}\" session expired, reconnect required",
869 server.name
870 ));
871 }
872 return Ok(client);
873 }
874 Err("No runtime available for connected server".into())
875 }
876 McpServerConnection::Failed(f) => Err(format!("MCP server '{}' failed: {}", f.name, f.error.as_deref().unwrap_or("unknown"))),
877 McpServerConnection::NeedsAuth(n) => {
878 Err(format!("MCP server '{}' requires authentication", n.name))
879 }
880 McpServerConnection::Pending(p) => {
881 Err(format!("MCP server '{}' not yet connected", p.name))
882 }
883 McpServerConnection::Disabled(d) => {
884 Err(format!("MCP server '{}' is disabled", d.name))
885 }
886 }
887}
888
889pub async fn reconnect_mcp_server(
891 name: &str,
892 config: &ScopedMcpServerConfig,
893) -> McpServerConnection {
894 clear_mcp_auth_cache();
895 connect_to_server(name, config).await
896}
897
898impl McpServerConfig {
903 pub fn type_variant(&self) -> &'static str {
905 match self {
906 McpServerConfig::Stdio(_) => "stdio",
907 McpServerConfig::Sse(_) => "sse",
908 McpServerConfig::SseIde(_) => "sse-ide",
909 McpServerConfig::WebSocketIde(_) => "ws-ide",
910 McpServerConfig::Http(_) => "http",
911 McpServerConfig::WebSocket(_) => "ws",
912 McpServerConfig::Sdk(_) => "sdk",
913 McpServerConfig::ClaudeAiProxy(_) => "claudeai-proxy",
914 }
915 }
916}
917
918pub fn infer_compact_schema(value: &serde_json::Value, depth: usize) -> String {
925 const MAX_ENTRIES: usize = 10;
926
927 match value {
928 serde_json::Value::Null => "null".to_string(),
929 serde_json::Value::Bool(_) => "boolean".to_string(),
930 serde_json::Value::Number(_) => "number".to_string(),
931 serde_json::Value::String(_) => "string".to_string(),
932 serde_json::Value::Array(arr) => {
933 if arr.is_empty() {
934 "[]".to_string()
935 } else {
936 let inner_depth = depth.saturating_sub(1);
937 format!("[{}]", infer_compact_schema(&arr[0], inner_depth))
938 }
939 }
940 serde_json::Value::Object(obj) => {
941 if depth == 0 {
942 "{...}".to_string()
943 } else {
944 let entries: Vec<String> = obj
945 .iter()
946 .take(MAX_ENTRIES)
947 .map(|(k, v)| {
948 format!(
949 "{}: {}",
950 k,
951 infer_compact_schema(v, depth.saturating_sub(1))
952 )
953 })
954 .collect();
955 format!("{{{}}}", entries.join(", "))
956 }
957 }
958 }
959}
960
961pub type MCPResultType = &'static str; #[derive(Debug, Clone)]
970pub struct TransformedMCPResult {
971 pub content: serde_json::Value,
972 pub result_type: MCPResultType,
973 pub schema: Option<String>,
974}