@@ -45,8 +45,8 @@ use crate::oauth::oauth_flow;
4545use crate :: prompt_template;
4646use crate :: subprocess:: configure_subprocess;
4747use rmcp:: model:: {
48- CallToolRequestParams , Content , ErrorCode , ErrorData , GetPromptResult , Prompt , Resource ,
49- ResourceContents , ServerInfo , Tool ,
48+ CallToolRequestParams , CallToolResult , Content , ErrorCode , ErrorData , GetPromptResult , Meta ,
49+ Prompt , Resource , ResourceContents , ServerInfo , Tool ,
5050} ;
5151use rmcp:: transport:: auth:: AuthClient ;
5252use schemars:: _private:: NoSerialize ;
@@ -121,6 +121,20 @@ pub struct ExtensionManagerCapabilities {
121121 pub host_info : Option < GooseMcpHostInfo > ,
122122}
123123
124+ #[ derive( Debug , Clone , serde:: Serialize ) ]
125+ #[ serde( rename_all = "camelCase" ) ]
126+ pub struct GooseMcpAppToolAttachment {
127+ pub tool_name : String ,
128+ pub extension_name : String ,
129+ pub resource_uri : String ,
130+ #[ serde( skip_serializing_if = "Option::is_none" ) ]
131+ pub tool_meta : Option < Value > ,
132+ #[ serde( skip_serializing_if = "Option::is_none" ) ]
133+ pub resource_result : Option < Value > ,
134+ #[ serde( skip_serializing_if = "Option::is_none" ) ]
135+ pub read_error : Option < String > ,
136+ }
137+
124138/// Manages goose extensions / MCP clients and their interactions
125139pub struct ExtensionManager {
126140 extensions : Mutex < HashMap < String , Extension > > ,
@@ -214,6 +228,42 @@ pub fn get_tool_owner(tool: &Tool) -> Option<String> {
214228 . map ( |s| s. to_string ( ) )
215229}
216230
231+ fn get_tool_meta_value ( tool : & Tool ) -> Option < Value > {
232+ tool. meta . as_ref ( ) . map ( |meta| Value :: Object ( meta. 0 . clone ( ) ) )
233+ }
234+
235+ fn get_tool_resource_uri ( tool : & Tool ) -> Option < String > {
236+ tool. meta
237+ . as_ref ( )
238+ . and_then ( |meta| meta. 0 . get ( "ui" ) )
239+ . and_then ( Value :: as_object)
240+ . and_then ( |ui| ui. get ( "resourceUri" ) )
241+ . and_then ( Value :: as_str)
242+ . map ( ToString :: to_string)
243+ }
244+
245+ fn merge_mcp_app_attachment_meta (
246+ result : & mut CallToolResult ,
247+ attachment : & GooseMcpAppToolAttachment ,
248+ ) {
249+ let mut meta_map = result
250+ . meta
251+ . as_ref ( )
252+ . map ( |meta| meta. 0 . clone ( ) )
253+ . unwrap_or_default ( ) ;
254+ let mut goose_value = meta_map
255+ . remove ( "goose" )
256+ . and_then ( |value| value. as_object ( ) . cloned ( ) )
257+ . unwrap_or_default ( ) ;
258+
259+ goose_value. insert (
260+ "mcpApp" . to_string ( ) ,
261+ serde_json:: to_value ( attachment) . unwrap_or ( Value :: Null ) ,
262+ ) ;
263+ meta_map. insert ( "goose" . to_string ( ) , Value :: Object ( goose_value) ) ;
264+ result. meta = Some ( Meta ( meta_map) ) ;
265+ }
266+
217267fn is_unprefixed_extension ( config : & ExtensionConfig ) -> bool {
218268 match config {
219269 ExtensionConfig :: Platform { name, .. } | ExtensionConfig :: Builtin { name, .. } => {
@@ -241,9 +291,12 @@ pub fn is_hidden_extension(name: &str) -> bool {
241291
242292/// Result of resolving a tool call to its owning extension
243293struct ResolvedTool {
294+ tool_name : String ,
244295 extension_name : String ,
245296 actual_tool_name : String ,
246297 client : McpClientBox ,
298+ tool_meta : Option < Value > ,
299+ resource_uri : Option < String > ,
247300}
248301
249302async fn child_process_client (
@@ -1064,6 +1117,55 @@ impl ExtensionManager {
10641117 Ok ( tools)
10651118 }
10661119
1120+ fn host_supports_mcp_apps ( & self ) -> bool {
1121+ if let Some ( host_info) = & self . capabilities . host_info {
1122+ if host_info. explicit_extensions {
1123+ return host_info. mcpui_enabled ( ) ;
1124+ }
1125+ }
1126+
1127+ self . capabilities . mcpui
1128+ }
1129+
1130+ async fn hydrate_mcp_app_attachment (
1131+ client : & McpClientBox ,
1132+ session_id : & str ,
1133+ resolved_tool : & ResolvedTool ,
1134+ cancellation_token : CancellationToken ,
1135+ result : & mut CallToolResult ,
1136+ ) {
1137+ if result. is_error == Some ( true ) {
1138+ return ;
1139+ }
1140+
1141+ let Some ( resource_uri) = resolved_tool. resource_uri . clone ( ) else {
1142+ return ;
1143+ } ;
1144+
1145+ let mut attachment = GooseMcpAppToolAttachment {
1146+ tool_name : resolved_tool. tool_name . clone ( ) ,
1147+ extension_name : resolved_tool. extension_name . clone ( ) ,
1148+ resource_uri : resource_uri. clone ( ) ,
1149+ tool_meta : resolved_tool. tool_meta . clone ( ) ,
1150+ resource_result : None ,
1151+ read_error : None ,
1152+ } ;
1153+
1154+ match client
1155+ . read_resource ( session_id, & resource_uri, cancellation_token)
1156+ . await
1157+ {
1158+ Ok ( resource_result) => {
1159+ attachment. resource_result = serde_json:: to_value ( & resource_result) . ok ( ) ;
1160+ }
1161+ Err ( error) => {
1162+ attachment. read_error = Some ( error. to_string ( ) ) ;
1163+ }
1164+ }
1165+
1166+ merge_mcp_app_attachment_meta ( result, & attachment) ;
1167+ }
1168+
10671169 async fn invalidate_tools_cache_and_bump_version ( & self ) {
10681170 self . tools_cache_version . fetch_add ( 1 , Ordering :: SeqCst ) ;
10691171 * self . tools_cache . lock ( ) . await = None ;
@@ -1433,17 +1535,6 @@ impl ExtensionManager {
14331535 session_id : & str ,
14341536 tool_name : & str ,
14351537 ) -> Result < ResolvedTool , ErrorData > {
1436- if let Some ( ( prefix, actual) ) = tool_name. split_once ( "__" ) {
1437- let owner = name_to_key ( prefix) ;
1438- if let Some ( client) = self . get_server_client ( & owner) . await {
1439- return Ok ( ResolvedTool {
1440- extension_name : owner,
1441- actual_tool_name : actual. to_string ( ) ,
1442- client,
1443- } ) ;
1444- }
1445- }
1446-
14471538 let tools = self . get_all_tools_cached ( session_id) . await . map_err ( |e| {
14481539 ErrorData :: new (
14491540 ErrorCode :: INTERNAL_ERROR ,
@@ -1453,13 +1544,19 @@ impl ExtensionManager {
14531544 } ) ?;
14541545
14551546 if let Some ( tool) = tools. iter ( ) . find ( |t| * t. name == * tool_name) {
1456- let owner = get_tool_owner ( tool) . ok_or_else ( || {
1457- ErrorData :: new (
1458- ErrorCode :: RESOURCE_NOT_FOUND ,
1459- format ! ( "Tool '{}' has no owner" , tool_name) ,
1460- None ,
1461- )
1462- } ) ?;
1547+ let owner = get_tool_owner ( tool)
1548+ . or_else ( || {
1549+ tool_name
1550+ . split_once ( "__" )
1551+ . map ( |( prefix, _) | name_to_key ( prefix) )
1552+ } )
1553+ . ok_or_else ( || {
1554+ ErrorData :: new (
1555+ ErrorCode :: RESOURCE_NOT_FOUND ,
1556+ format ! ( "Tool '{}' has no owner" , tool_name) ,
1557+ None ,
1558+ )
1559+ } ) ?;
14631560
14641561 let actual_tool_name = tool_name
14651562 . strip_prefix ( & format ! ( "{owner}__" ) )
@@ -1475,12 +1572,29 @@ impl ExtensionManager {
14751572 } ) ?;
14761573
14771574 return Ok ( ResolvedTool {
1575+ tool_name : tool. name . to_string ( ) ,
14781576 extension_name : owner,
14791577 actual_tool_name,
14801578 client,
1579+ tool_meta : get_tool_meta_value ( tool) ,
1580+ resource_uri : get_tool_resource_uri ( tool) ,
14811581 } ) ;
14821582 }
14831583
1584+ if let Some ( ( prefix, actual) ) = tool_name. split_once ( "__" ) {
1585+ let owner = name_to_key ( prefix) ;
1586+ if let Some ( client) = self . get_server_client ( & owner) . await {
1587+ return Ok ( ResolvedTool {
1588+ tool_name : tool_name. to_string ( ) ,
1589+ extension_name : owner,
1590+ actual_tool_name : actual. to_string ( ) ,
1591+ client,
1592+ tool_meta : None ,
1593+ resource_uri : None ,
1594+ } ) ;
1595+ }
1596+ }
1597+
14841598 Err ( ErrorData :: new (
14851599 ErrorCode :: RESOURCE_NOT_FOUND ,
14861600 format ! ( "Tool '{}' not found" , tool_name) ,
@@ -1516,8 +1630,13 @@ impl ExtensionManager {
15161630
15171631 let arguments = tool_call. arguments . clone ( ) ;
15181632 let client = resolved. client . clone ( ) ;
1633+ let hydration_client = client. clone ( ) ;
15191634 let notifications_receiver = client. subscribe ( ) . await ;
1520- let actual_tool_name = resolved. actual_tool_name ;
1635+ let actual_tool_name = resolved. actual_tool_name . clone ( ) ;
1636+ let resolved_tool = resolved;
1637+ let should_hydrate_mcp_app = self . host_supports_mcp_apps ( ) ;
1638+ let read_cancellation_token = cancellation_token. clone ( ) ;
1639+ let session_id = ctx. session_id . clone ( ) ;
15211640 let owned_ctx = ToolCallContext :: new (
15221641 ctx. session_id . clone ( ) ,
15231642 ctx. working_dir . clone ( ) ,
@@ -1531,15 +1650,28 @@ impl ExtensionManager {
15311650 owned_ctx. session_id,
15321651 owned_ctx. working_dir,
15331652 ) ;
1534- client
1653+ let mut result = client
15351654 . call_tool ( & owned_ctx, & actual_tool_name, arguments, cancellation_token)
15361655 . await
15371656 . map_err ( |e| match e {
15381657 ServiceError :: McpError ( error_data) => error_data,
15391658 _ => {
15401659 ErrorData :: new ( ErrorCode :: INTERNAL_ERROR , e. to_string ( ) , e. maybe_to_value ( ) )
15411660 }
1542- } )
1661+ } ) ?;
1662+
1663+ if should_hydrate_mcp_app {
1664+ Self :: hydrate_mcp_app_attachment (
1665+ & hydration_client,
1666+ & session_id,
1667+ & resolved_tool,
1668+ read_cancellation_token,
1669+ & mut result,
1670+ )
1671+ . await ;
1672+ }
1673+
1674+ Ok ( result)
15431675 } ;
15441676
15451677 Ok ( ToolCallResult {
0 commit comments