MCP Apps Plumbing (#6286)

Co-authored-by: Douwe Osinga <douwe@squareup.com>
This commit is contained in:
Douwe Osinga
2026-01-05 10:34:52 -05:00
committed by GitHub
parent d1f242b69f
commit 85aee9c5b7
21 changed files with 484 additions and 947 deletions
+1
View File
@@ -1176,6 +1176,7 @@ impl Agent {
request.id.clone(),
request.tool_call.clone(),
request.metadata.as_ref(),
request.tool_meta.clone(),
);
messages_to_add.push(request_msg);
let final_response = tool_response_messages[idx]
+33 -67
View File
@@ -41,8 +41,8 @@ use crate::oauth::oauth_flow;
use crate::prompt_template;
use crate::subprocess::configure_command_no_window;
use rmcp::model::{
CallToolRequestParam, Content, ErrorCode, ErrorData, GetPromptResult, Prompt, RawContent,
Resource, ResourceContents, ServerInfo, Tool,
CallToolRequestParam, Content, ErrorCode, ErrorData, GetPromptResult, Prompt, Resource,
ResourceContents, ServerInfo, Tool,
};
use rmcp::transport::auth::AuthClient;
use schemars::_private::NoSerialize;
@@ -713,14 +713,13 @@ impl ExtensionManager {
input_schema: tool.input_schema,
annotations: tool.annotations,
output_schema: tool.output_schema,
icons: None,
title: None,
meta: None,
icons: tool.icons,
title: tool.title,
meta: tool.meta,
});
}
}
// Exit loop when there are no more pages
if client_tools.next_cursor.is_none() {
break;
}
@@ -773,7 +772,7 @@ impl ExtensionManager {
}
// Function that gets executed for read_resource tool
pub async fn read_resource(
pub async fn read_resource_tool(
&self,
params: Value,
cancellation_token: CancellationToken,
@@ -784,14 +783,17 @@ impl ExtensionManager {
// If extension name is provided, we can just look it up
if extension_name.is_some() {
let result = self
.read_resource_from_extension(
uri,
extension_name.unwrap(),
cancellation_token.clone(),
true,
)
let read_result = self
.read_resource(uri, extension_name.unwrap(), cancellation_token.clone())
.await?;
let mut result = Vec::new();
for content in read_result.contents {
if let ResourceContents::TextResourceContents { text, .. } = content {
let content_str = format!("{}\n\n{}", uri, text);
result.push(Content::text(content_str));
}
}
return Ok(result);
}
@@ -804,16 +806,20 @@ impl ExtensionManager {
let extension_names: Vec<String> = self.extensions.lock().await.keys().cloned().collect();
for extension_name in extension_names {
let result = self
.read_resource_from_extension(
uri,
&extension_name,
cancellation_token.clone(),
true,
)
let read_result = self
.read_resource(uri, &extension_name, cancellation_token.clone())
.await;
match result {
Ok(result) => return Ok(result),
match read_result {
Ok(read_result) => {
let mut result = Vec::new();
for content in read_result.contents {
if let ResourceContents::TextResourceContents { text, .. } = content {
let content_str = format!("{}\n\n{}", uri, text);
result.push(Content::text(content_str));
}
}
return Ok(result);
}
Err(_) => continue,
}
}
@@ -839,13 +845,12 @@ impl ExtensionManager {
))
}
async fn read_resource_from_extension(
pub async fn read_resource(
&self,
uri: &str,
extension_name: &str,
cancellation_token: CancellationToken,
format_with_uri: bool,
) -> Result<Vec<Content>, ErrorData> {
) -> Result<rmcp::model::ReadResourceResult, ErrorData> {
let available_extensions = self
.extensions
.lock()
@@ -865,7 +870,7 @@ impl ExtensionManager {
.ok_or(ErrorData::new(ErrorCode::INVALID_PARAMS, error_msg, None))?;
let client_guard = client.lock().await;
let read_result = client_guard
client_guard
.read_resource(uri, cancellation_token)
.await
.map_err(|_| {
@@ -874,21 +879,7 @@ impl ExtensionManager {
format!("Could not read resource with uri: {}", uri),
None,
)
})?;
let mut result = Vec::new();
for content in read_result.contents {
if let ResourceContents::TextResourceContents { text, .. } = content {
let content_str = if format_with_uri {
format!("{}\n\n{}", uri, text)
} else {
text
};
result.push(Content::text(content_str));
}
}
Ok(result)
})
}
pub async fn get_ui_resources(&self) -> Result<Vec<(String, Resource)>, ErrorData> {
@@ -925,31 +916,6 @@ impl ExtensionManager {
Ok(ui_resources)
}
pub async fn read_ui_resource(
&self,
uri: &str,
extension_name: &str,
cancellation_token: CancellationToken,
) -> Result<String, ErrorData> {
let contents = self
.read_resource_from_extension(uri, extension_name, cancellation_token, false)
.await?;
contents
.into_iter()
.find_map(|c| match c.raw {
RawContent::Text(text_content) => Some(text_content.text),
_ => None,
})
.ok_or_else(|| {
ErrorData::new(
ErrorCode::RESOURCE_NOT_FOUND,
format!("No text content in resource '{}'", uri),
None,
)
})
}
async fn list_resources_from_extension(
&self,
extension_name: &str,
@@ -257,7 +257,7 @@ impl ExtensionManagerClient {
.unwrap_or(serde_json::Value::Object(serde_json::Map::new()));
match extension_manager
.read_resource(params, tokio_util::sync::CancellationToken::default())
.read_resource_tool(params, tokio_util::sync::CancellationToken::default())
.await
{
Ok(content) => Ok(content),
+23 -12
View File
@@ -274,6 +274,10 @@ impl Agent {
let schema_value = Value::Object(tool.input_schema.as_ref().clone());
tool_call.arguments =
coerce_tool_arguments(tool_call.arguments.clone(), &schema_value);
if let Some(ref meta) = tool.meta {
coerced_req.tool_meta = serde_json::to_value(meta).ok();
}
}
}
@@ -286,22 +290,29 @@ impl Agent {
// Create a filtered message with frontend tool requests removed
let mut filtered_content = Vec::new();
let mut tool_request_index = 0;
// Process each content item one by one
for content in &response.content {
let should_include = match content {
MessageContent::ToolRequest(req) => {
if let Ok(tool_call) = &req.tool_call {
!self.is_frontend_tool(&tool_call.name).await
} else {
true
match content {
MessageContent::ToolRequest(_) => {
if tool_request_index < tool_requests.len() {
let coerced_req = &tool_requests[tool_request_index];
tool_request_index += 1;
let should_include = if let Ok(tool_call) = &coerced_req.tool_call {
!self.is_frontend_tool(&tool_call.name).await
} else {
true
};
if should_include {
filtered_content.push(MessageContent::ToolRequest(coerced_req.clone()));
}
}
}
_ => true,
};
if should_include {
filtered_content.push(content.clone());
_ => {
filtered_content.push(content.clone());
}
}
}