Skip to main content

aster/agents/
extension_manager_extension.rs

1use crate::agents::extension::PlatformExtensionContext;
2use crate::agents::mcp_client::{Error, McpClientTrait};
3use crate::config::get_extension_by_name;
4use anyhow::Result;
5use async_trait::async_trait;
6use indoc::indoc;
7use rmcp::model::{
8    CallToolResult, Content, ErrorCode, ErrorData, GetPromptResult, Implementation,
9    InitializeResult, JsonObject, ListPromptsResult, ListResourcesResult, ListToolsResult,
10    ProtocolVersion, ReadResourceResult, ServerCapabilities, ServerNotification, Tool,
11    ToolAnnotations, ToolsCapability,
12};
13use schemars::{schema_for, JsonSchema};
14use serde::{Deserialize, Serialize};
15use serde_json::Value;
16use std::sync::Arc;
17use tokio::sync::mpsc;
18use tokio_util::sync::CancellationToken;
19use tracing::error;
20
21pub static EXTENSION_NAME: &str = "Extension Manager";
22// pub static DISPLAY_NAME: &str = "Extension Manager";
23
24#[derive(Debug, thiserror::Error)]
25pub enum ExtensionManagerToolError {
26    #[error("Unknown tool: {tool_name}")]
27    UnknownTool { tool_name: String },
28
29    #[error("Extension manager not available")]
30    ManagerUnavailable,
31
32    #[error("Missing required parameter: {param_name}")]
33    MissingParameter { param_name: String },
34
35    #[error("Invalid action: {action}. Must be 'enable' or 'disable'")]
36    InvalidAction { action: String },
37
38    #[error("Extension operation failed: {message}")]
39    OperationFailed { message: String },
40
41    #[error("Failed to deserialize parameters: {0}")]
42    DeserializationError(#[from] serde_json::Error),
43}
44
45#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
46#[serde(rename_all = "lowercase")]
47pub enum ManageExtensionAction {
48    Enable,
49    Disable,
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
53pub struct ManageExtensionsParams {
54    pub action: ManageExtensionAction,
55    pub extension_name: String,
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
59pub struct ReadResourceParams {
60    pub uri: String,
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub extension_name: Option<String>,
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
66pub struct ListResourcesParams {
67    #[serde(skip_serializing_if = "Option::is_none")]
68    pub extension_name: Option<String>,
69}
70
71pub const READ_RESOURCE_TOOL_NAME: &str = "read_resource";
72pub const LIST_RESOURCES_TOOL_NAME: &str = "list_resources";
73pub const SEARCH_AVAILABLE_EXTENSIONS_TOOL_NAME: &str = "search_available_extensions";
74pub const MANAGE_EXTENSIONS_TOOL_NAME: &str = "manage_extensions";
75pub const MANAGE_EXTENSIONS_TOOL_NAME_COMPLETE: &str = "extensionmanager__manage_extensions";
76
77pub struct ExtensionManagerClient {
78    info: InitializeResult,
79    #[allow(dead_code)]
80    context: PlatformExtensionContext,
81}
82
83impl ExtensionManagerClient {
84    pub fn new(context: PlatformExtensionContext) -> Result<Self> {
85        let info = InitializeResult {
86            protocol_version: ProtocolVersion::V_2025_03_26,
87            capabilities: ServerCapabilities {
88                tools: Some(ToolsCapability {
89                    list_changed: Some(false),
90                }),
91                resources: None,
92                prompts: None,
93                completions: None,
94                experimental: None,
95                logging: None,
96            },
97            server_info: Implementation {
98                name: EXTENSION_NAME.to_string(),
99                title: Some(EXTENSION_NAME.to_string()),
100                version: "1.0.0".to_string(),
101                icons: None,
102                website_url: None,
103            },
104            instructions: Some(indoc! {r#"
105                Extension Management
106
107                Use these tools to discover, enable, and disable extensions, as well as review resources.
108
109                Available tools:
110                - search_available_extensions: Find extensions available to enable/disable
111                - manage_extensions: Enable or disable extensions
112                - list_resources: List resources from extensions
113                - read_resource: Read specific resources from extensions
114
115                Use search_available_extensions when you need to find what extensions are available.
116                Use manage_extensions to enable or disable specific extensions by name.
117                Use list_resources and read_resource to work with extension data and resources.
118            "#}.to_string()),
119        };
120
121        Ok(Self { info, context })
122    }
123
124    async fn handle_search_available_extensions(
125        &self,
126    ) -> Result<Vec<Content>, ExtensionManagerToolError> {
127        if let Some(weak_ref) = &self.context.extension_manager {
128            if let Some(extension_manager) = weak_ref.upgrade() {
129                match extension_manager.search_available_extensions().await {
130                    Ok(content) => Ok(content),
131                    Err(e) => Err(ExtensionManagerToolError::OperationFailed {
132                        message: format!("Failed to search available extensions: {}", e.message),
133                    }),
134                }
135            } else {
136                Err(ExtensionManagerToolError::ManagerUnavailable)
137            }
138        } else {
139            Err(ExtensionManagerToolError::ManagerUnavailable)
140        }
141    }
142
143    async fn handle_manage_extensions(
144        &self,
145        arguments: Option<JsonObject>,
146    ) -> Result<Vec<Content>, ExtensionManagerToolError> {
147        let arguments = arguments.ok_or(ExtensionManagerToolError::MissingParameter {
148            param_name: "arguments".to_string(),
149        })?;
150
151        let params: ManageExtensionsParams =
152            serde_json::from_value(serde_json::Value::Object(arguments))?;
153
154        match self
155            .manage_extensions_impl(params.action, params.extension_name)
156            .await
157        {
158            Ok(content) => Ok(content),
159            Err(error_data) => Err(ExtensionManagerToolError::OperationFailed {
160                message: error_data.message.to_string(),
161            }),
162        }
163    }
164
165    async fn manage_extensions_impl(
166        &self,
167        action: ManageExtensionAction,
168        extension_name: String,
169    ) -> Result<Vec<Content>, ErrorData> {
170        let extension_manager = self
171            .context
172            .extension_manager
173            .as_ref()
174            .and_then(|weak| weak.upgrade())
175            .ok_or_else(|| {
176                ErrorData::new(
177                    ErrorCode::INTERNAL_ERROR,
178                    "Extension manager is no longer available".to_string(),
179                    None,
180                )
181            })?;
182
183        if action == ManageExtensionAction::Disable {
184            return extension_manager
185                .remove_extension(&extension_name)
186                .await
187                .map(|_| {
188                    vec![Content::text(format!(
189                        "The extension '{}' has been disabled successfully",
190                        extension_name
191                    ))]
192                })
193                .map_err(|e| ErrorData::new(ErrorCode::INTERNAL_ERROR, e.to_string(), None));
194        }
195
196        let config = match get_extension_by_name(&extension_name) {
197            Some(config) => config,
198            None => {
199                return Err(ErrorData::new(
200                    ErrorCode::RESOURCE_NOT_FOUND,
201                    format!(
202                        "Extension '{}' not found. Please check the extension name and try again.",
203                        extension_name
204                    ),
205                    None,
206                ));
207            }
208        };
209
210        extension_manager
211            .add_extension(config)
212            .await
213            .map(|_| {
214                vec![Content::text(format!(
215                    "The extension '{}' has been installed successfully",
216                    extension_name
217                ))]
218            })
219            .map_err(|e| ErrorData::new(ErrorCode::INTERNAL_ERROR, e.to_string(), None))
220    }
221
222    async fn handle_list_resources(
223        &self,
224        arguments: Option<JsonObject>,
225    ) -> Result<Vec<Content>, ExtensionManagerToolError> {
226        if let Some(weak_ref) = &self.context.extension_manager {
227            if let Some(extension_manager) = weak_ref.upgrade() {
228                let params = arguments
229                    .map(serde_json::Value::Object)
230                    .unwrap_or(serde_json::Value::Object(serde_json::Map::new()));
231
232                match extension_manager
233                    .list_resources(params, tokio_util::sync::CancellationToken::default())
234                    .await
235                {
236                    Ok(content) => Ok(content),
237                    Err(e) => Err(ExtensionManagerToolError::OperationFailed {
238                        message: format!("Failed to list resources: {}", e.message),
239                    }),
240                }
241            } else {
242                Err(ExtensionManagerToolError::ManagerUnavailable)
243            }
244        } else {
245            Err(ExtensionManagerToolError::ManagerUnavailable)
246        }
247    }
248
249    async fn handle_read_resource(
250        &self,
251        arguments: Option<JsonObject>,
252    ) -> Result<Vec<Content>, ExtensionManagerToolError> {
253        if let Some(weak_ref) = &self.context.extension_manager {
254            if let Some(extension_manager) = weak_ref.upgrade() {
255                let params = arguments
256                    .map(serde_json::Value::Object)
257                    .unwrap_or(serde_json::Value::Object(serde_json::Map::new()));
258
259                match extension_manager
260                    .read_resource_tool(params, tokio_util::sync::CancellationToken::default())
261                    .await
262                {
263                    Ok(content) => Ok(content),
264                    Err(e) => Err(ExtensionManagerToolError::OperationFailed {
265                        message: format!("Failed to read resource: {}", e.message),
266                    }),
267                }
268            } else {
269                Err(ExtensionManagerToolError::ManagerUnavailable)
270            }
271        } else {
272            Err(ExtensionManagerToolError::ManagerUnavailable)
273        }
274    }
275
276    #[allow(clippy::too_many_lines)]
277    async fn get_tools(&self) -> Vec<Tool> {
278        let mut tools = vec![
279            Tool::new(
280                SEARCH_AVAILABLE_EXTENSIONS_TOOL_NAME.to_string(),
281                "Searches for additional extensions available to help complete tasks.
282        Use this tool when you're unable to find a specific feature or functionality you need to complete your task, or when standard approaches aren't working.
283        These extensions might provide the exact tools needed to solve your problem.
284        If you find a relevant one, consider using your tools to enable it.".to_string(),
285                Arc::new(
286                    serde_json::json!({
287                        "type": "object",
288                        "required": [],
289                        "properties": {}
290                    })
291                    .as_object()
292                    .expect("Schema must be an object")
293                    .clone()
294                ),
295            ).annotate(ToolAnnotations {
296                title: Some("Discover extensions".to_string()),
297                read_only_hint: Some(true),
298                destructive_hint: Some(false),
299                idempotent_hint: Some(false),
300                open_world_hint: Some(false),
301            }),
302            Tool::new(
303                MANAGE_EXTENSIONS_TOOL_NAME.to_string(),
304                "Tool to manage extensions and tools in aster context.
305            Enable or disable extensions to help complete tasks.
306            Enable or disable an extension by providing the extension name.
307            ".to_string(),
308                Arc::new(
309                    serde_json::to_value(schema_for!(ManageExtensionsParams))
310                        .expect("Failed to serialize schema")
311                        .as_object()
312                        .expect("Schema must be an object")
313                        .clone()
314                ),
315            ).annotate(ToolAnnotations {
316                title: Some("Enable or disable an extension".to_string()),
317                read_only_hint: Some(false),
318                destructive_hint: Some(false),
319                idempotent_hint: Some(false),
320                open_world_hint: Some(false),
321            }),
322        ];
323
324        // Only add resource tools if extension manager supports resources
325        if let Some(weak_ref) = &self.context.extension_manager {
326            if let Some(extension_manager) = weak_ref.upgrade() {
327                if extension_manager.supports_resources().await {
328                    tools.extend([
329                        Tool::new(
330                            LIST_RESOURCES_TOOL_NAME.to_string(),
331                            indoc! {r#"
332            List resources from an extension(s).
333
334            Resources allow extensions to share data that provide context to LLMs, such as
335            files, database schemas, or application-specific information. This tool lists resources
336            in the provided extension, and returns a list for the user to browse. If no extension
337            is provided, the tool will search all extensions for the resource.
338        "#}.to_string(),
339                            Arc::new(
340                                serde_json::to_value(schema_for!(ListResourcesParams))
341                                    .expect("Failed to serialize schema")
342                                    .as_object()
343                                    .expect("Schema must be an object")
344                                    .clone()
345                            ),
346                        ).annotate(ToolAnnotations {
347                            title: Some("List resources".to_string()),
348                            read_only_hint: Some(true),
349                            destructive_hint: Some(false),
350                            idempotent_hint: Some(false),
351                            open_world_hint: Some(false),
352                        }),
353                        Tool::new(
354                            READ_RESOURCE_TOOL_NAME.to_string(),
355                            indoc! {r#"
356            Read a resource from an extension.
357
358            Resources allow extensions to share data that provide context to LLMs, such as
359            files, database schemas, or application-specific information. This tool searches for the
360            resource URI in the provided extension, and reads in the resource content. If no extension
361            is provided, the tool will search all extensions for the resource.
362        "#}.to_string(),
363                            Arc::new(
364                                serde_json::to_value(schema_for!(ReadResourceParams))
365                                    .expect("Failed to serialize schema")
366                                    .as_object()
367                                    .expect("Schema must be an object")
368                                    .clone()
369                            ),
370                        ).annotate(ToolAnnotations {
371                            title: Some("Read a resource".to_string()),
372                            read_only_hint: Some(true),
373                            destructive_hint: Some(false),
374                            idempotent_hint: Some(false),
375                            open_world_hint: Some(false),
376                        }),
377                    ]);
378                }
379            }
380        }
381
382        tools
383    }
384}
385
386#[async_trait]
387impl McpClientTrait for ExtensionManagerClient {
388    async fn list_resources(
389        &self,
390        _next_cursor: Option<String>,
391        _cancellation_token: CancellationToken,
392    ) -> Result<ListResourcesResult, Error> {
393        Err(Error::TransportClosed)
394    }
395
396    async fn read_resource(
397        &self,
398        _uri: &str,
399        _cancellation_token: CancellationToken,
400    ) -> Result<ReadResourceResult, Error> {
401        // Extension manager doesn't expose resources directly
402        Err(Error::TransportClosed)
403    }
404
405    async fn list_tools(
406        &self,
407        _next_cursor: Option<String>,
408        _cancellation_token: CancellationToken,
409    ) -> Result<ListToolsResult, Error> {
410        Ok(ListToolsResult {
411            tools: self.get_tools().await,
412            next_cursor: None,
413            meta: None,
414        })
415    }
416
417    async fn call_tool(
418        &self,
419        name: &str,
420        arguments: Option<JsonObject>,
421        _cancellation_token: CancellationToken,
422    ) -> Result<CallToolResult, Error> {
423        let result = match name {
424            SEARCH_AVAILABLE_EXTENSIONS_TOOL_NAME => {
425                self.handle_search_available_extensions().await
426            }
427            MANAGE_EXTENSIONS_TOOL_NAME => self.handle_manage_extensions(arguments).await,
428            LIST_RESOURCES_TOOL_NAME => self.handle_list_resources(arguments).await,
429            READ_RESOURCE_TOOL_NAME => self.handle_read_resource(arguments).await,
430            _ => Err(ExtensionManagerToolError::UnknownTool {
431                tool_name: name.to_string(),
432            }),
433        };
434
435        match result {
436            Ok(content) => Ok(CallToolResult::success(content)),
437            Err(error) => {
438                // Log the error for debugging
439                error!("Extension manager tool '{}' failed: {}", name, error);
440
441                // Return proper error result with is_error flag set
442                Ok(CallToolResult {
443                    content: vec![Content::text(error.to_string())],
444                    is_error: Some(true), // ✅ Properly mark as error
445                    structured_content: None,
446                    meta: None,
447                })
448            }
449        }
450    }
451
452    async fn list_prompts(
453        &self,
454        _next_cursor: Option<String>,
455        _cancellation_token: CancellationToken,
456    ) -> Result<ListPromptsResult, Error> {
457        Err(Error::TransportClosed)
458    }
459
460    async fn get_prompt(
461        &self,
462        _name: &str,
463        _arguments: Value,
464        _cancellation_token: CancellationToken,
465    ) -> Result<GetPromptResult, Error> {
466        Err(Error::TransportClosed)
467    }
468
469    async fn subscribe(&self) -> mpsc::Receiver<ServerNotification> {
470        mpsc::channel(1).1
471    }
472
473    fn get_info(&self) -> Option<&InitializeResult> {
474        Some(&self.info)
475    }
476}