use crate::{AgentToolOutput, AnyAgentTool, ToolCallEventStream, ToolInput};use agent_client_protocol::schema::v1 as acp;use anyhow::Result;use collections::{BTreeMap, HashMap};use context_server::{ContextServerId, client::NotificationSubscription};use futures::FutureExt as _;use gpui::{App, AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task};use language_model::{LanguageModelImage, LanguageModelImageExt, LanguageModelToolResultContent};use project::context_server_store::{ContextServerStatus, ContextServerStore};use std::sync::Arc;use util::{ResultExt, markdown::MarkdownEscaped}; /// Maximum number of characters to show from a tool argument in the/// collapsed tool-call header. Longer values are truncated with an ellipsis.const MAX_INLINE_ARG_LEN: usize = 120; /// Generates a tool ID for an MCP tool that can be used in settings.////// The format is `mcp:<server_id>:<tool_name>` to avoid collisions with built-in tools.pub fn mcp_tool_id(server_id: &str, tool_name: &str) -> String { format!("mcp:{}:{}", server_id, tool_name)} pub struct ContextServerPrompt { pub server_id: ContextServerId, pub prompt: context_server::types::Prompt,} pub enum ContextServerRegistryEvent { ToolsChanged, PromptsChanged,} impl EventEmitter<ContextServerRegistryEvent> for ContextServerRegistry {} pub struct ContextServerRegistry { server_store: Entity<ContextServerStore>, registered_servers: HashMap<ContextServerId, RegisteredContextServer>, _subscription: gpui::Subscription,} struct RegisteredContextServer { tools: BTreeMap<SharedString, Arc<dyn AnyAgentTool>>, prompts: BTreeMap<SharedString, ContextServerPrompt>, load_tools: Task<Result<()>>, load_prompts: Task<Result<()>>, _tools_updated_subscription: Option<NotificationSubscription>,} impl ContextServerRegistry { pub fn new(server_store: Entity<ContextServerStore>, cx: &mut Context<Self>) -> Self { let mut this = Self { server_store: server_store.clone(), registered_servers: HashMap::default(), _subscription: cx.subscribe(&server_store, Self::handle_context_server_store_event), }; for server in server_store.read(cx).running_servers() { this.reload_tools_for_server(server.id(), cx); this.reload_prompts_for_server(server.id(), cx); } this } pub fn tools_for_server( &self, server_id: &ContextServerId, ) -> impl Iterator<Item = &Arc<dyn AnyAgentTool>> { self.registered_servers .get(server_id) .map(|server| server.tools.values()) .into_iter() .flatten() } pub fn servers( &self, ) -> impl Iterator< Item = ( &ContextServerId, &BTreeMap<SharedString, Arc<dyn AnyAgentTool>>, ), > { self.registered_servers .iter() .map(|(id, server)| (id, &server.tools)) } pub fn prompts(&self) -> impl Iterator<Item = &ContextServerPrompt> { self.registered_servers .values() .flat_map(|server| server.prompts.values()) } pub fn find_prompt( &self, server_id: Option<&ContextServerId>, name: &str, ) -> Option<&ContextServerPrompt> { if let Some(server_id) = server_id { self.registered_servers .get(server_id) .and_then(|server| server.prompts.get(name)) } else { self.registered_servers .values() .find_map(|server| server.prompts.get(name)) } } pub fn server_store(&self) -> &Entity<ContextServerStore> { &self.server_store } fn get_or_register_server( &mut self, server_id: &ContextServerId, cx: &mut Context<Self>, ) -> &mut RegisteredContextServer { self.registered_servers .entry(server_id.clone()) .or_insert_with(|| Self::init_registered_server(server_id, &self.server_store, cx)) } fn init_registered_server( server_id: &ContextServerId, server_store: &Entity<ContextServerStore>, cx: &mut Context<Self>, ) -> RegisteredContextServer { let tools_updated_subscription = server_store .read(cx) .get_running_server(server_id) .and_then(|server| { let client = server.client()?; if !client.capable(context_server::protocol::ServerCapability::Tools) { return None; } let server_id = server.id(); let this = cx.entity().downgrade(); Some(client.on_notification( "notifications/tools/list_changed", Box::new(move |_params, cx: AsyncApp| { let server_id = server_id.clone(); let this = this.clone(); cx.spawn(async move |cx| { this.update(cx, |this, cx| { log::info!( "Received tools/list_changed notification for server {}", server_id ); this.reload_tools_for_server(server_id, cx); }) }) .detach(); }), )) }); RegisteredContextServer { tools: BTreeMap::default(), prompts: BTreeMap::default(), load_tools: Task::ready(Ok(())), load_prompts: Task::ready(Ok(())), _tools_updated_subscription: tools_updated_subscription, } } fn reload_tools_for_server(&mut self, server_id: ContextServerId, cx: &mut Context<Self>) { let Some(server) = self.server_store.read(cx).get_running_server(&server_id) else { return; }; let Some(client) = server.client() else { return; }; if !client.capable(context_server::protocol::ServerCapability::Tools) { return; } let registered_server = self.get_or_register_server(&server_id, cx); registered_server.load_tools = cx.spawn(async move |this, cx| { let response = client .request::<context_server::types::requests::ListTools>(()) .await; this.update(cx, |this, cx| { let Some(registered_server) = this.registered_servers.get_mut(&server_id) else { return; }; registered_server.tools.clear(); if let Some(response) = response.log_err() { for tool in response.tools { let tool = Arc::new(ContextServerTool::new( this.server_store.clone(), server.id(), tool, )); registered_server.tools.insert(tool.name(), tool); } cx.emit(ContextServerRegistryEvent::ToolsChanged); cx.notify(); } }) }); } fn reload_prompts_for_server(&mut self, server_id: ContextServerId, cx: &mut Context<Self>) { let Some(server) = self.server_store.read(cx).get_running_server(&server_id) else { return; }; let Some(client) = server.client() else { return; }; if !client.capable(context_server::protocol::ServerCapability::Prompts) { return; } let registered_server = self.get_or_register_server(&server_id, cx); registered_server.load_prompts = cx.spawn(async move |this, cx| { let response = client .request::<context_server::types::requests::PromptsList>(()) .await; this.update(cx, |this, cx| { let Some(registered_server) = this.registered_servers.get_mut(&server_id) else { return; }; registered_server.prompts.clear(); if let Some(response) = response.log_err() { for prompt in response.prompts { let name: SharedString = prompt.name.clone().into(); registered_server.prompts.insert( name, ContextServerPrompt { server_id: server_id.clone(), prompt, }, ); } cx.emit(ContextServerRegistryEvent::PromptsChanged); cx.notify(); } }) }); } fn handle_context_server_store_event( &mut self, _: Entity<ContextServerStore>, event: &project::context_server_store::ServerStatusChangedEvent, cx: &mut Context<Self>, ) { let project::context_server_store::ServerStatusChangedEvent { server_id, status } = event; match status { ContextServerStatus::Starting | ContextServerStatus::Authenticating => {} ContextServerStatus::Running => { self.reload_tools_for_server(server_id.clone(), cx); self.reload_prompts_for_server(server_id.clone(), cx); } ContextServerStatus::Stopped | ContextServerStatus::Error(_) | ContextServerStatus::AuthRequired | ContextServerStatus::ClientSecretRequired { .. } => { if let Some(registered_server) = self.registered_servers.remove(server_id) { if !registered_server.tools.is_empty() { cx.emit(ContextServerRegistryEvent::ToolsChanged); } if !registered_server.prompts.is_empty() { cx.emit(ContextServerRegistryEvent::PromptsChanged); } } cx.notify(); } }; }} struct ContextServerTool { store: Entity<ContextServerStore>, server_id: ContextServerId, tool: context_server::types::Tool,} impl ContextServerTool { fn new( store: Entity<ContextServerStore>, server_id: ContextServerId, tool: context_server::types::Tool, ) -> Self { Self { store, server_id, tool, } }} impl AnyAgentTool for ContextServerTool { fn name(&self) -> SharedString { self.tool.name.clone().into() } fn description(&self) -> SharedString { self.tool.description.clone().unwrap_or_default().into() } fn kind(&self) -> acp::ToolKind { acp::ToolKind::Other } fn initial_title(&self, input: serde_json::Value, _cx: &mut App) -> SharedString { format_mcp_initial_title(&self.tool.name, &input).into() } fn input_schema( &self, format: language_model::LanguageModelToolSchemaFormat, ) -> Result<serde_json::Value> { let mut schema = self.tool.input_schema.clone(); language_model::tool_schema::adapt_schema_to_format(&mut schema, format)?; Ok(match schema { serde_json::Value::Null => { serde_json::json!({ "type": "object", "properties": [] }) } serde_json::Value::Object(map) if map.is_empty() => { serde_json::json!({ "type": "object", "properties": [] }) } _ => schema, }) } fn run( self: Arc<Self>, input: ToolInput<serde_json::Value>, event_stream: ToolCallEventStream, cx: &mut App, ) -> Task<Result<AgentToolOutput, AgentToolOutput>> { let Some(server) = self.store.read(cx).get_running_server(&self.server_id) else { return Task::ready(Err(anyhow::anyhow!("Context server not found").into())); }; let tool_name = self.tool.name.clone(); let tool_id = mcp_tool_id(&self.server_id.0, &self.tool.name); let display_name = self.tool.name.clone(); let initial_title = self.initial_title(serde_json::Value::Null, cx); let authorize = event_stream.authorize_third_party_tool(initial_title, tool_id, display_name, cx); cx.spawn(async move |cx| { let input = input .recv() .await .map_err(|e| anyhow::anyhow!(e.to_string()))?; authorize .await .map_err(|e| anyhow::anyhow!(e.to_string()))?; let Some(protocol) = server.client() else { return Err(anyhow::anyhow!("Context server not initialized").into()); }; let arguments = if let serde_json::Value::Object(map) = input { Some(map.into_iter().collect()) } else { None }; log::trace!( "Running tool: {} with arguments: {:?}", tool_name, arguments ); let request = protocol.request::<context_server::types::requests::CallTool>( context_server::types::CallToolParams { name: tool_name, arguments, meta: None, }, ); let response = futures::select! { response = request.fuse() => response?, _ = event_stream.cancelled_by_user().fuse() => { return Err(anyhow::anyhow!("MCP tool cancelled by user").into()); } }; if response.is_error == Some(true) { let error_message: String = response.content.iter().filter_map(|c| c.text()).collect(); return Err(anyhow::anyhow!(error_message).into()); } let mut llm_output = Vec::new(); let mut tool_call_content = Vec::new(); let mut concatenated_text = String::new(); for content in response.content { match content { context_server::types::ToolResponseContent::Text { text } => { concatenated_text.push_str(&text); tool_call_content.push(acp::ToolCallContent::Content(acp::Content::new( acp::ContentBlock::Text(acp::TextContent::new(text.clone())), ))); llm_output.push(LanguageModelToolResultContent::Text(text.into())); } context_server::types::ToolResponseContent::Image { data, mime_type } => { tool_call_content.push(acp::ToolCallContent::Content(acp::Content::new( acp::ContentBlock::Image(acp::ImageContent::new( data.clone(), mime_type.clone(), )), ))); let language_model_image = cx .background_spawn({ let mime_type = mime_type.clone(); async move { LanguageModelImage::from_base64_image(&data, &mime_type) } }) .await; match language_model_image { Ok(Some(image)) => { llm_output.push(LanguageModelToolResultContent::Image(image)); } Ok(None) => { log::warn!( "Skipping MCP tool response image with MIME type `{}` because it cannot be converted for language model input", mime_type ); } Err(error) => { log::warn!( "Failed to convert MCP tool response image with MIME type `{}` for language model input: {:#}", mime_type, error ); } } } context_server::types::ToolResponseContent::Audio { .. } => { log::warn!("Ignoring audio content from tool response"); } context_server::types::ToolResponseContent::Resource { .. } => { log::warn!("Ignoring resource content from tool response"); } context_server::types::ToolResponseContent::ResourceLink { .. } => { log::warn!("Ignoring resource link content from tool response"); } } } if !tool_call_content.is_empty() { event_stream .update_fields(acp::ToolCallUpdateFields::new().content(tool_call_content)); } let raw_output = serde_json::Value::String(concatenated_text); Ok(AgentToolOutput { raw_output, llm_output, }) }) } fn replay( &self, _input: serde_json::Value, _output: serde_json::Value, _event_stream: ToolCallEventStream, _cx: &mut App, ) -> Result<()> { Ok(()) }} /// Builds the header label shown for an MCP tool call. When the input is an/// object with a single string-valued field, the value is inlined next to the/// tool name so the primary argument (e.g. a URL, path, or query) is visible/// without expanding the input block — matching the UX of built-in tools like/// `Fetch`. All other shapes fall back to the tool name alone.fn format_mcp_initial_title(tool_name: &str, input: &serde_json::Value) -> String { if let Some(value) = single_string_arg(input) { let preview = truncate_chars(value, MAX_INLINE_ARG_LEN); format!("Run MCP tool `{}` {}", tool_name, MarkdownEscaped(&preview)) } else { format!("Run MCP tool `{}`", tool_name) }} fn single_string_arg(input: &serde_json::Value) -> Option<&str> { let obj = input.as_object()?; if obj.len() != 1 { return None; } obj.values().next()?.as_str()Lines 1–500 of 658. Load more
use crate::{AgentToolOutput, AnyAgentTool, ToolCallEventStream, ToolInput}; use agent_client_protocol::schema::v1 as acp; use anyhow::Result; use collections::{BTreeMap, HashMap}; use context_server::{ContextServerId, client::NotificationSubscription}; use futures::FutureExt as _; use gpui::{App, AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task}; use language_model::{LanguageModelImage, LanguageModelImageExt, LanguageModelToolResultContent}; use project::context_server_store::{ContextServerStatus, ContextServerStore}; use std::sync::Arc; use util::{ResultExt, markdown::MarkdownEscaped}; /// Maximum number of characters to show from a tool argument in the /// collapsed tool-call header. Longer values are truncated with an ellipsis. const MAX_INLINE_ARG_LEN: usize = 120; /// Generates a tool ID for an MCP tool that can be used in settings. /// /// The format is `mcp:<server_id>:<tool_name>` to avoid collisions with built-in tools. pub fn mcp_tool_id(server_id: &str, tool_name: &str) -> String { format!("mcp:{}:{}", server_id, tool_name) } pub struct ContextServerPrompt { pub server_id: ContextServerId, pub prompt: context_server::types::Prompt, } pub enum ContextServerRegistryEvent { ToolsChanged, PromptsChanged, } impl EventEmitter<ContextServerRegistryEvent> for ContextServerRegistry {} pub struct ContextServerRegistry { server_store: Entity<ContextServerStore>, registered_servers: HashMap<ContextServerId, RegisteredContextServer>, _subscription: gpui::Subscription, } struct RegisteredContextServer { tools: BTreeMap<SharedString, Arc<dyn AnyAgentTool>>, prompts: BTreeMap<SharedString, ContextServerPrompt>, load_tools: Task<Result<()>>, load_prompts: Task<Result<()>>, _tools_updated_subscription: Option<NotificationSubscription>, } impl ContextServerRegistry { pub fn new(server_store: Entity<ContextServerStore>, cx: &mut Context<Self>) -> Self { let mut this = Self { server_store: server_store.clone(), registered_servers: HashMap::default(), _subscription: cx.subscribe(&server_store, Self::handle_context_server_store_event), }; for server in server_store.read(cx).running_servers() { this.reload_tools_for_server(server.id(), cx); this.reload_prompts_for_server(server.id(), cx); } this } pub fn tools_for_server( &self, server_id: &ContextServerId, ) -> impl Iterator<Item = &Arc<dyn AnyAgentTool>> { self.registered_servers .get(server_id) .map(|server| server.tools.values()) .into_iter() .flatten() } pub fn servers( &self, ) -> impl Iterator< Item = ( &ContextServerId, &BTreeMap<SharedString, Arc<dyn AnyAgentTool>>, ), > { self.registered_servers .iter() .map(|(id, server)| (id, &server.tools)) } pub fn prompts(&self) -> impl Iterator<Item = &ContextServerPrompt> { self.registered_servers .values() .flat_map(|server| server.prompts.values()) } pub fn find_prompt( &self, server_id: Option<&ContextServerId>, name: &str, ) -> Option<&ContextServerPrompt> { if let Some(server_id) = server_id { self.registered_servers .get(server_id) .and_then(|server| server.prompts.get(name)) } else { self.registered_servers .values() .find_map(|server| server.prompts.get(name)) } } pub fn server_store(&self) -> &Entity<ContextServerStore> { &self.server_store } fn get_or_register_server( &mut self, server_id: &ContextServerId, cx: &mut Context<Self>, ) -> &mut RegisteredContextServer { self.registered_servers .entry(server_id.clone()) .or_insert_with(|| Self::init_registered_server(server_id, &self.server_store, cx)) } fn init_registered_server( server_id: &ContextServerId, server_store: &Entity<ContextServerStore>, cx: &mut Context<Self>, ) -> RegisteredContextServer { let tools_updated_subscription = server_store .read(cx) .get_running_server(server_id) .and_then(|server| { let client = server.client()?; if !client.capable(context_server::protocol::ServerCapability::Tools) { return None; } let server_id = server.id(); let this = cx.entity().downgrade(); Some(client.on_notification( "notifications/tools/list_changed", Box::new(move |_params, cx: AsyncApp| { let server_id = server_id.clone(); let this = this.clone(); cx.spawn(async move |cx| { this.update(cx, |this, cx| { log::info!( "Received tools/list_changed notification for server {}", server_id ); this.reload_tools_for_server(server_id, cx); }) }) .detach(); }), )) }); RegisteredContextServer { tools: BTreeMap::default(), prompts: BTreeMap::default(), load_tools: Task::ready(Ok(())), load_prompts: Task::ready(Ok(())), _tools_updated_subscription: tools_updated_subscription, } } fn reload_tools_for_server(&mut self, server_id: ContextServerId, cx: &mut Context<Self>) { let Some(server) = self.server_store.read(cx).get_running_server(&server_id) else { return; }; let Some(client) = server.client() else { return; }; if !client.capable(context_server::protocol::ServerCapability::Tools) { return; } let registered_server = self.get_or_register_server(&server_id, cx); registered_server.load_tools = cx.spawn(async move |this, cx| { let response = client .request::<context_server::types::requests::ListTools>(()) .await; this.update(cx, |this, cx| { let Some(registered_server) = this.registered_servers.get_mut(&server_id) else { return; }; registered_server.tools.clear(); if let Some(response) = response.log_err() { for tool in response.tools { let tool = Arc::new(ContextServerTool::new( this.server_store.clone(), server.id(), tool, )); registered_server.tools.insert(tool.name(), tool); } cx.emit(ContextServerRegistryEvent::ToolsChanged); cx.notify(); } }) }); } fn reload_prompts_for_server(&mut self, server_id: ContextServerId, cx: &mut Context<Self>) { let Some(server) = self.server_store.read(cx).get_running_server(&server_id) else { return; }; let Some(client) = server.client() else { return; }; if !client.capable(context_server::protocol::ServerCapability::Prompts) { return; } let registered_server = self.get_or_register_server(&server_id, cx); registered_server.load_prompts = cx.spawn(async move |this, cx| { let response = client .request::<context_server::types::requests::PromptsList>(()) .await; this.update(cx, |this, cx| { let Some(registered_server) = this.registered_servers.get_mut(&server_id) else { return; }; registered_server.prompts.clear(); if let Some(response) = response.log_err() { for prompt in response.prompts { let name: SharedString = prompt.name.clone().into(); registered_server.prompts.insert( name, ContextServerPrompt { server_id: server_id.clone(), prompt, }, ); } cx.emit(ContextServerRegistryEvent::PromptsChanged); cx.notify(); } }) }); } fn handle_context_server_store_event( &mut self, _: Entity<ContextServerStore>, event: &project::context_server_store::ServerStatusChangedEvent, cx: &mut Context<Self>, ) { let project::context_server_store::ServerStatusChangedEvent { server_id, status } = event; match status { ContextServerStatus::Starting | ContextServerStatus::Authenticating => {} ContextServerStatus::Running => { self.reload_tools_for_server(server_id.clone(), cx); self.reload_prompts_for_server(server_id.clone(), cx); } ContextServerStatus::Stopped | ContextServerStatus::Error(_) | ContextServerStatus::AuthRequired | ContextServerStatus::ClientSecretRequired { .. } => { if let Some(registered_server) = self.registered_servers.remove(server_id) { if !registered_server.tools.is_empty() { cx.emit(ContextServerRegistryEvent::ToolsChanged); } if !registered_server.prompts.is_empty() { cx.emit(ContextServerRegistryEvent::PromptsChanged); } } cx.notify(); } }; } } struct ContextServerTool { store: Entity<ContextServerStore>, server_id: ContextServerId, tool: context_server::types::Tool, } impl ContextServerTool { fn new( store: Entity<ContextServerStore>, server_id: ContextServerId, tool: context_server::types::Tool, ) -> Self { Self { store, server_id, tool, } } } impl AnyAgentTool for ContextServerTool { fn name(&self) -> SharedString { self.tool.name.clone().into() } fn description(&self) -> SharedString { self.tool.description.clone().unwrap_or_default().into() } fn kind(&self) -> acp::ToolKind { acp::ToolKind::Other } fn initial_title(&self, input: serde_json::Value, _cx: &mut App) -> SharedString { format_mcp_initial_title(&self.tool.name, &input).into() } fn input_schema( &self, format: language_model::LanguageModelToolSchemaFormat, ) -> Result<serde_json::Value> { let mut schema = self.tool.input_schema.clone(); language_model::tool_schema::adapt_schema_to_format(&mut schema, format)?; Ok(match schema { serde_json::Value::Null => { serde_json::json!({ "type": "object", "properties": [] }) } serde_json::Value::Object(map) if map.is_empty() => { serde_json::json!({ "type": "object", "properties": [] }) } _ => schema, }) } fn run( self: Arc<Self>, input: ToolInput<serde_json::Value>, event_stream: ToolCallEventStream, cx: &mut App, ) -> Task<Result<AgentToolOutput, AgentToolOutput>> { let Some(server) = self.store.read(cx).get_running_server(&self.server_id) else { return Task::ready(Err(anyhow::anyhow!("Context server not found").into())); }; let tool_name = self.tool.name.clone(); let tool_id = mcp_tool_id(&self.server_id.0, &self.tool.name); let display_name = self.tool.name.clone(); let initial_title = self.initial_title(serde_json::Value::Null, cx); let authorize = event_stream.authorize_third_party_tool(initial_title, tool_id, display_name, cx); cx.spawn(async move |cx| { let input = input .recv() .await .map_err(|e| anyhow::anyhow!(e.to_string()))?; authorize .await .map_err(|e| anyhow::anyhow!(e.to_string()))?; let Some(protocol) = server.client() else { return Err(anyhow::anyhow!("Context server not initialized").into()); }; let arguments = if let serde_json::Value::Object(map) = input { Some(map.into_iter().collect()) } else { None }; log::trace!( "Running tool: {} with arguments: {:?}", tool_name, arguments ); let request = protocol.request::<context_server::types::requests::CallTool>( context_server::types::CallToolParams { name: tool_name, arguments, meta: None, }, ); let response = futures::select! { response = request.fuse() => response?, _ = event_stream.cancelled_by_user().fuse() => { return Err(anyhow::anyhow!("MCP tool cancelled by user").into()); } }; if response.is_error == Some(true) { let error_message: String = response.content.iter().filter_map(|c| c.text()).collect(); return Err(anyhow::anyhow!(error_message).into()); } let mut llm_output = Vec::new(); let mut tool_call_content = Vec::new(); let mut concatenated_text = String::new(); for content in response.content { match content { context_server::types::ToolResponseContent::Text { text } => { concatenated_text.push_str(&text); tool_call_content.push(acp::ToolCallContent::Content(acp::Content::new( acp::ContentBlock::Text(acp::TextContent::new(text.clone())), ))); llm_output.push(LanguageModelToolResultContent::Text(text.into())); } context_server::types::ToolResponseContent::Image { data, mime_type } => { tool_call_content.push(acp::ToolCallContent::Content(acp::Content::new( acp::ContentBlock::Image(acp::ImageContent::new( data.clone(), mime_type.clone(), )), ))); let language_model_image = cx .background_spawn({ let mime_type = mime_type.clone(); async move { LanguageModelImage::from_base64_image(&data, &mime_type) } }) .await; match language_model_image { Ok(Some(image)) => { llm_output.push(LanguageModelToolResultContent::Image(image)); } Ok(None) => { log::warn!( "Skipping MCP tool response image with MIME type `{}` because it cannot be converted for language model input", mime_type ); } Err(error) => { log::warn!( "Failed to convert MCP tool response image with MIME type `{}` for language model input: {:#}", mime_type, error ); } } } context_server::types::ToolResponseContent::Audio { .. } => { log::warn!("Ignoring audio content from tool response"); } context_server::types::ToolResponseContent::Resource { .. } => { log::warn!("Ignoring resource content from tool response"); } context_server::types::ToolResponseContent::ResourceLink { .. } => { log::warn!("Ignoring resource link content from tool response"); } } } if !tool_call_content.is_empty() { event_stream .update_fields(acp::ToolCallUpdateFields::new().content(tool_call_content)); } let raw_output = serde_json::Value::String(concatenated_text); Ok(AgentToolOutput { raw_output, llm_output, }) }) } fn replay( &self, _input: serde_json::Value, _output: serde_json::Value, _event_stream: ToolCallEventStream, _cx: &mut App, ) -> Result<()> { Ok(()) } } /// Builds the header label shown for an MCP tool call. When the input is an /// object with a single string-valued field, the value is inlined next to the /// tool name so the primary argument (e.g. a URL, path, or query) is visible /// without expanding the input block — matching the UX of built-in tools like /// `Fetch`. All other shapes fall back to the tool name alone. fn format_mcp_initial_title(tool_name: &str, input: &serde_json::Value) -> String { if let Some(value) = single_string_arg(input) { let preview = truncate_chars(value, MAX_INLINE_ARG_LEN); format!("Run MCP tool `{}` {}", tool_name, MarkdownEscaped(&preview)) } else { format!("Run MCP tool `{}`", tool_name) } } fn single_string_arg(input: &serde_json::Value) -> Option<&str> { let obj = input.as_object()?; if obj.len() != 1 { return None; } obj.values().next()?.as_str()