fix(extension-manager): require extension_name on read_resource (#8989)

Signed-off-by: olaservo <olahungerford@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ola Hungerford
2026-05-07 12:51:51 -07:00
committed by GitHub
parent c80044d9d0
commit 23b4754643
2 changed files with 26 additions and 79 deletions
+10 -67
View File
@@ -1304,76 +1304,19 @@ impl ExtensionManager {
cancellation_token: CancellationToken, cancellation_token: CancellationToken,
) -> Result<Vec<Content>, ErrorData> { ) -> Result<Vec<Content>, ErrorData> {
let uri = require_str_parameter(&params, "uri")?; let uri = require_str_parameter(&params, "uri")?;
let extension_name = require_str_parameter(&params, "extension_name")?;
let extension_name = params.get("extension_name").and_then(|v| v.as_str()); let read_result = self
.read_resource(session_id, uri, extension_name, cancellation_token)
.await?;
// If extension name is provided, we can just look it up let mut result = Vec::new();
if let Some(ext_name) = extension_name { for content in read_result.contents {
let read_result = self if let ResourceContents::TextResourceContents { text, .. } = content {
.read_resource(session_id, uri, ext_name, cancellation_token.clone()) result.push(Content::text(format!("{}\n\n{}", uri, text)));
.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);
}
// If extension name is not provided, we need to search for the resource across all extensions
// Loop through each extension and try to read the resource, don't raise an error if the resource is not found
// TODO: do we want to find if a provided uri is in multiple extensions?
// currently it will return the first match and skip any others
let extension_names: Vec<String> = self
.extensions
.lock()
.await
.iter()
.filter(|(_name, ext)| ext.supports_resources())
.map(|(name, _)| name.clone())
.collect();
for extension_name in extension_names {
let read_result = self
.read_resource(session_id, uri, &extension_name, cancellation_token.clone())
.await;
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,
} }
} }
Ok(result)
// None of the extensions had the resource so we raise an error
let available_extensions = self
.extensions
.lock()
.await
.keys()
.map(|s| s.as_str())
.collect::<Vec<&str>>()
.join(", ");
let error_msg = format!(
"Resource with uri '{}' not found. Here are the available extensions: {}",
uri, available_extensions
);
Err(ErrorData::new(
ErrorCode::RESOURCE_NOT_FOUND,
error_msg,
None,
))
} }
pub async fn read_resource( pub async fn read_resource(
@@ -1493,7 +1436,7 @@ impl ExtensionManager {
params: Value, params: Value,
cancellation_token: CancellationToken, cancellation_token: CancellationToken,
) -> Result<Vec<Content>, ErrorData> { ) -> Result<Vec<Content>, ErrorData> {
let extension = params.get("extension").and_then(|v| v.as_str()); let extension = params.get("extension_name").and_then(|v| v.as_str());
match extension { match extension {
Some(extension_name) => { Some(extension_name) => {
@@ -53,8 +53,7 @@ pub struct ManageExtensionsParams {
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct ReadResourceParams { pub struct ReadResourceParams {
pub uri: String, pub uri: String,
#[serde(skip_serializing_if = "Option::is_none")] pub extension_name: String,
pub extension_name: Option<String>,
} }
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
@@ -325,15 +324,17 @@ impl ExtensionManagerClient {
files, database schemas, or application-specific information. This tool lists resources files, database schemas, or application-specific information. This tool lists resources
in the provided extension, and returns a list for the user to browse. If no extension in the provided extension, and returns a list for the user to browse. If no extension
is provided, the tool will search all extensions for the resource. is provided, the tool will search all extensions for the resource.
"#}.to_string(), "#}
.to_string(),
Arc::new( Arc::new(
serde_json::to_value(schema_for!(ListResourcesParams)) serde_json::to_value(schema_for!(ListResourcesParams))
.expect("Failed to serialize schema") .expect("Failed to serialize schema")
.as_object() .as_object()
.expect("Schema must be an object") .expect("Schema must be an object")
.clone() .clone(),
), ),
).annotate(ToolAnnotations::from_raw( )
.annotate(ToolAnnotations::from_raw(
Some("List resources".to_string()), Some("List resources".to_string()),
Some(true), Some(true),
Some(false), Some(false),
@@ -343,21 +344,24 @@ impl ExtensionManagerClient {
Tool::new( Tool::new(
READ_RESOURCE_TOOL_NAME.to_string(), READ_RESOURCE_TOOL_NAME.to_string(),
indoc! {r#" indoc! {r#"
Read a resource from an extension. Read a resource from a specific extension.
Resources allow extensions to share data that provide context to LLMs, such as Resources allow extensions to share data that provide context to LLMs, such as
files, database schemas, or application-specific information. This tool searches for the files, database schemas, or application-specific information. You must pass the
resource URI in the provided extension, and reads in the resource content. If no extension owning extension as `extension_name`; if you don't know which extension owns a
is provided, the tool will search all extensions for the resource. URI, call `list_resources` first — its output labels each resource with its
"#}.to_string(), extension.
"#}
.to_string(),
Arc::new( Arc::new(
serde_json::to_value(schema_for!(ReadResourceParams)) serde_json::to_value(schema_for!(ReadResourceParams))
.expect("Failed to serialize schema") .expect("Failed to serialize schema")
.as_object() .as_object()
.expect("Schema must be an object") .expect("Schema must be an object")
.clone() .clone(),
), ),
).annotate(ToolAnnotations::from_raw( )
.annotate(ToolAnnotations::from_raw(
Some("Read a resource".to_string()), Some("Read a resource".to_string()),
Some(true), Some(true),
Some(false), Some(false),