diff --git a/crates/goose-cli/src/cli.rs b/crates/goose-cli/src/cli.rs index ea5ca42ca4..1b09fd1114 100644 --- a/crates/goose-cli/src/cli.rs +++ b/crates/goose-cli/src/cli.rs @@ -7,7 +7,6 @@ use crate::commands::acp::run_acp_agent; use crate::commands::bench::agent_generator; use crate::commands::configure::handle_configure; use crate::commands::info::handle_info; -use crate::commands::mcp::run_server; use crate::commands::project::{handle_project_default, handle_projects_interactive}; use crate::commands::recipe::{handle_deeplink, handle_list, handle_validate}; // Import the new handlers from commands::schedule @@ -743,7 +742,8 @@ pub async fn cli() -> Result<()> { return Ok(()); } Some(Command::Mcp { name }) => { - let _ = run_server(&name).await; + crate::logging::setup_logging(Some(&format!("mcp-{name}")), None)?; + let _ = goose_mcp::mcp_server_runner::run_mcp_server(&name).await; } Some(Command::Acp {}) => { let _ = run_acp_agent().await; diff --git a/crates/goose-cli/src/commands/mcp.rs b/crates/goose-cli/src/commands/mcp.rs deleted file mode 100644 index 14cf395a69..0000000000 --- a/crates/goose-cli/src/commands/mcp.rs +++ /dev/null @@ -1,123 +0,0 @@ -use anyhow::{anyhow, Result}; -use goose_mcp::{ - AutoVisualiserRouter, ComputerControllerRouter, DeveloperServer, MemoryServer, TutorialServer, -}; -use mcp_server::router::RouterService; -use mcp_server::{BoundedService, ByteTransport, Server}; -use rmcp::{transport::stdio, ServiceExt}; -use tokio::io::{stdin, stdout}; - -use std::sync::Arc; -use tokio::sync::Notify; - -#[cfg(unix)] -use nix::sys::signal::{kill, Signal}; -#[cfg(unix)] -use nix::unistd::getpgrp; -#[cfg(unix)] -use nix::unistd::Pid; - -pub async fn run_server(name: &str) -> Result<()> { - crate::logging::setup_logging(Some(&format!("mcp-{name}")), None)?; - - if name == "googledrive" || name == "google_drive" { - return Err(anyhow!( - "the built-in Google Drive extension has been removed" - )); - } - - tracing::info!("Starting MCP server"); - - // Handle RMCP-based servers - if name == "developer" { - let service = DeveloperServer::new() - .serve(stdio()) - .await - .inspect_err(|e| { - tracing::error!("serving error: {:?}", e); - })?; - - service.waiting().await?; - return Ok(()); - } - - if name == "autovisualiser" { - let service = AutoVisualiserRouter::new() - .serve(stdio()) - .await - .inspect_err(|e| { - tracing::error!("serving error: {:?}", e); - })?; - - service.waiting().await?; - return Ok(()); - } - - if name == "tutorial" { - let service = TutorialServer::new() - .serve(stdio()) - .await - .inspect_err(|e| { - tracing::error!("serving error: {:?}", e); - })?; - - service.waiting().await?; - return Ok(()); - } - - if name == "memory" { - let service = MemoryServer::new().serve(stdio()).await.inspect_err(|e| { - tracing::error!("serving error: {:?}", e); - })?; - - service.waiting().await?; - return Ok(()); - } - - // Handle old MCP-based servers - if name == "memory" { - let service = MemoryServer::new().serve(stdio()).await.inspect_err(|e| { - tracing::error!("serving error: {:?}", e); - })?; - service.waiting().await?; - return Ok(()); - } - - // Handle old MCP-based servers - let router: Option> = match name { - "computercontroller" => Some(Box::new(RouterService(ComputerControllerRouter::new()))), - _ => None, - }; - - let shutdown = Arc::new(Notify::new()); - let shutdown_clone = shutdown.clone(); - - tokio::spawn(async move { - crate::signal::shutdown_signal().await; - shutdown_clone.notify_one(); - }); - - let server = Server::new(router.unwrap_or_else(|| panic!("Unknown server requested {}", name))); - let transport = ByteTransport::new(stdin(), stdout()); - - tracing::info!("Server initialized and ready to handle requests"); - - tokio::select! { - result = server.run(transport) => { - Ok(result?) - } - _ = shutdown.notified() => { - // On Unix systems, kill the entire process group - #[cfg(unix)] - { - fn terminate_process_group() { - let pgid = getpgrp(); - kill(Pid::from_raw(-pgid.as_raw()), Signal::SIGTERM) - .expect("Failed to send SIGTERM to process group"); - } - terminate_process_group(); - } - Ok(()) - } - } -} diff --git a/crates/goose-cli/src/commands/mod.rs b/crates/goose-cli/src/commands/mod.rs index 43b666de7d..698fd02fdf 100644 --- a/crates/goose-cli/src/commands/mod.rs +++ b/crates/goose-cli/src/commands/mod.rs @@ -2,7 +2,6 @@ pub mod acp; pub mod bench; pub mod configure; pub mod info; -pub mod mcp; pub mod project; pub mod recipe; pub mod schedule; diff --git a/crates/goose-mcp/src/computercontroller/mod.rs b/crates/goose-mcp/src/computercontroller/mod.rs index 8edd4cd59e..8f03bd0d93 100644 --- a/crates/goose-mcp/src/computercontroller/mod.rs +++ b/crates/goose-mcp/src/computercontroller/mod.rs @@ -1,29 +1,24 @@ -use base64::Engine; use etcetera::{choose_app_strategy, AppStrategy}; use indoc::{formatdoc, indoc}; use reqwest::{Client, Url}; -use serde_json::Value; -use std::borrow::Cow; -use std::{ - collections::HashMap, fs, future::Future, path::PathBuf, pin::Pin, sync::Arc, sync::Mutex, +use rmcp::{ + handler::server::{router::tool::ToolRouter, wrapper::Parameters}, + model::{ + CallToolResult, Content, ErrorCode, ErrorData, Implementation, ListResourcesResult, + PaginatedRequestParam, RawResource, ReadResourceRequestParam, ReadResourceResult, Resource, + ResourceContents, ServerCapabilities, ServerInfo, + }, + schemars::JsonSchema, + service::RequestContext, + tool, tool_handler, tool_router, RoleServer, ServerHandler, }; -use tokio::{process::Command, sync::mpsc}; +use serde::{Deserialize, Serialize}; +use std::{collections::HashMap, fs, path::PathBuf, sync::Arc, sync::Mutex}; +use tokio::process::Command; #[cfg(unix)] use std::os::unix::fs::PermissionsExt; -use mcp_core::{ - handler::{require_str_parameter, require_u64_parameter, PromptError, ResourceError}, - protocol::ServerCapabilities, -}; -use mcp_server::router::CapabilitiesBuilder; -use mcp_server::Router; -use rmcp::model::{ - AnnotateAble, Content, ErrorCode, ErrorData, JsonRpcMessage, Prompt, RawResource, Resource, - Tool, ToolAnnotations, -}; -use rmcp::object; - mod docx_tool; mod pdf_tool; mod xlsx_tool; @@ -31,396 +26,275 @@ mod xlsx_tool; mod platform; use platform::{create_system_automation, SystemAutomation}; -/// An extension designed for non-developers to help them with common tasks like -/// web scraping, data processing, and automation. +/// Enum for save_as parameter in web_scrape tool +#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone, Default)] +#[serde(rename_all = "lowercase")] +pub enum SaveAsFormat { + /// Save as text (for HTML pages) + #[default] + Text, + /// Save as JSON (for API responses) + Json, + /// Save as binary (for images and other files) + Binary, +} + +/// Parameters for the web_scrape tool +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct WebScrapeParams { + /// The URL to fetch content from + pub url: String, + /// How to interpret and save the content + #[serde(default)] + pub save_as: SaveAsFormat, +} + +/// Enum for language parameter in automation_script tool +#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)] +#[serde(rename_all = "lowercase")] +pub enum ScriptLanguage { + /// Shell/Bash script + Shell, + /// Batch script (Windows) + Batch, + /// Ruby script + Ruby, + /// PowerShell script + Powershell, +} + +/// Enum for command parameter in cache tool +#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)] +#[serde(rename_all = "lowercase")] +pub enum CacheCommand { + /// List all cached files + List, + /// View content of a cached file + View, + /// Delete a cached file + Delete, + /// Clear all cached files + Clear, +} + +/// Parameters for the automation_script tool +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct AutomationScriptParams { + /// The scripting language to use + #[serde(rename = "language")] + pub language: ScriptLanguage, + /// The script content + pub script: String, + /// Whether to save the script output to a file + #[serde(default)] + pub save_output: bool, +} + +/// Parameters for the computer_control tool +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct ComputerControlParams { + /// The automation script content (PowerShell for Windows, AppleScript for macOS) + pub script: String, + /// Whether to save the script output to a file + #[serde(default)] + pub save_output: bool, +} + +/// Parameters for the cache tool +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct CacheParams { + /// The command to perform + pub command: CacheCommand, + /// Path to the cached file for view/delete commands + pub path: Option, +} + +/// Parameters for the pdf_tool +/// Enum for operation parameter in pdf_tool +#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)] +#[serde(rename_all = "snake_case")] +pub enum PdfOperation { + /// Extract all text content from the PDF + ExtractText, + /// Extract and save embedded images to PNG files + ExtractImages, +} + +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct PdfToolParams { + /// Path to the PDF file + pub path: String, + /// Operation to perform on the PDF + pub operation: PdfOperation, +} + +/// Enum for operation parameter in docx_tool +#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)] +#[serde(rename_all = "snake_case")] +pub enum DocxOperation { + /// Extract all text content and structure from the DOCX + ExtractText, + /// Create a new DOCX or update existing one with provided content + UpdateDoc, +} + +/// Enum for update mode in docx_tool params +#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone, Default)] +#[serde(rename_all = "snake_case")] +pub enum DocxUpdateMode { + /// Add content to end of document (default) + #[default] + Append, + /// Replace specific text with new content + Replace, + /// Add content with specific heading level and styling + Structured, + /// Add an image to the document (with optional caption) + AddImage, +} + +/// Enum for text alignment in docx_tool params +#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)] +#[serde(rename_all = "lowercase")] +pub enum TextAlignment { + /// Left alignment + Left, + /// Center alignment + Center, + /// Right alignment + Right, + /// Justified alignment + Justified, +} + +/// Styling options for text in docx_tool +#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone, Default)] +pub struct DocxTextStyle { + /// Make text bold + #[serde(skip_serializing_if = "Option::is_none")] + pub bold: Option, + /// Make text italic + #[serde(skip_serializing_if = "Option::is_none")] + pub italic: Option, + /// Make text underlined + #[serde(skip_serializing_if = "Option::is_none")] + pub underline: Option, + /// Font size in points + #[serde(skip_serializing_if = "Option::is_none")] + pub size: Option, + /// Text color in hex format (e.g., 'FF0000' for red) + #[serde(skip_serializing_if = "Option::is_none")] + pub color: Option, + /// Text alignment + #[serde(skip_serializing_if = "Option::is_none")] + pub alignment: Option, +} + +/// Additional parameters for update_doc operation +#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone, Default)] +pub struct DocxUpdateParams { + /// Update mode (default: append) + #[serde(default)] + pub mode: DocxUpdateMode, + /// Text to replace (required for replace mode) + #[serde(skip_serializing_if = "Option::is_none")] + pub old_text: Option, + /// Heading level for structured mode (e.g., 'Heading1', 'Heading2') + #[serde(skip_serializing_if = "Option::is_none")] + pub level: Option, + /// Path to the image file (required for add_image mode) + #[serde(skip_serializing_if = "Option::is_none")] + pub image_path: Option, + /// Image width in pixels (optional) + #[serde(skip_serializing_if = "Option::is_none")] + pub width: Option, + /// Image height in pixels (optional) + #[serde(skip_serializing_if = "Option::is_none")] + pub height: Option, + /// Styling options for the text + #[serde(skip_serializing_if = "Option::is_none")] + pub style: Option, +} + +/// Parameters for the docx_tool +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct DocxToolParams { + /// Path to the DOCX file + pub path: String, + /// Operation to perform on the DOCX + pub operation: DocxOperation, + /// Content to write (required for update_doc operation) + #[serde(skip_serializing_if = "Option::is_none")] + pub content: Option, + /// Additional parameters for update_doc operation + #[serde(skip_serializing_if = "Option::is_none")] + pub params: Option, +} + +/// Parameters for the xlsx_tool +/// Enum for operation parameter in xlsx_tool +#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)] +#[serde(rename_all = "snake_case")] +pub enum XlsxOperation { + /// List all worksheets in the workbook + ListWorksheets, + /// Get column names from a worksheet + GetColumns, + /// Get values and formulas from a cell range + GetRange, + /// Search for text in a worksheet + FindText, + /// Update a single cell's value + UpdateCell, + /// Get value and formula from a specific cell + GetCell, + /// Save changes back to the file + Save, +} + +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct XlsxToolParams { + /// Path to the XLSX file + pub path: String, + /// Operation to perform on the XLSX file + pub operation: XlsxOperation, + /// Worksheet name (if not provided, uses first worksheet) + pub worksheet: Option, + /// Cell range in A1 notation (e.g., 'A1:C10') for get_range operation + pub range: Option, + /// Text to search for in find_text operation + pub search_text: Option, + /// Whether search should be case-sensitive + #[serde(default)] + pub case_sensitive: bool, + /// Row number for update_cell and get_cell operations + pub row: Option, + /// Column number for update_cell and get_cell operations + pub col: Option, + /// New value for update_cell operation + pub value: Option, +} + +/// ComputerController MCP Server using official RMCP SDK #[derive(Clone)] -pub struct ComputerControllerRouter { - tools: Vec, +pub struct ComputerControllerServer { + tool_router: ToolRouter, cache_dir: PathBuf, - active_resources: Arc>>, + active_resources: Arc>>, http_client: Client, instructions: String, system_automation: Arc>, } -impl Default for ComputerControllerRouter { +impl Default for ComputerControllerServer { fn default() -> Self { Self::new() } } -impl ComputerControllerRouter { +#[tool_router(router = tool_router)] +impl ComputerControllerServer { pub fn new() -> Self { - let web_scrape_tool = Tool::new( - "web_scrape", - indoc! {r#" - Fetch and save content from a web page. The content can be saved as: - - text (for HTML pages) - - json (for API responses) - - binary (for images and other files) - - The content is cached locally and can be accessed later using the cache_path - returned in the response. - "#}, - object!({ - "type": "object", - "required": ["url"], - "properties": { - "url": { - "type": "string", - "description": "The URL to fetch content from" - }, - "save_as": { - "type": "string", - "enum": ["text", "json", "binary"], - "default": "text", - "description": "How to interpret and save the content" - } - } - }), - ) - .annotate(ToolAnnotations { - title: Some("Web Scrape".to_string()), - read_only_hint: Some(true), - destructive_hint: Some(false), - idempotent_hint: Some(false), - open_world_hint: Some(true), - }); - - let computer_control_desc = match std::env::consts::OS { - "windows" => indoc! {r#" - Control the computer using Windows system automation. - - Features available: - - PowerShell automation for system control - - UI automation through PowerShell - - File and system management - - Windows-specific features and settings - - Can be combined with screenshot tool for visual task assistance. - "#}, - "macos" => indoc! {r#" - Control the computer using AppleScript (macOS only). Automate applications and system features. - - Key capabilities: - - Control Applications: Launch, quit, manage apps (Mail, Safari, iTunes, etc) - - Interact with app-specific feature: (e.g, edit documents, process photos) - - Perform tasks in third-party apps that support AppleScript - - UI Automation: Simulate user interactions like, clicking buttons, select menus, type text, filling out forms - - System Control: Manage settings (volume, brightness, wifi), shutdown/restart, monitor events - - Web & Email: Open URLs, web automation, send/organize emails, handle attachments - - Media: Manage music libraries, photo collections, playlists - - File Operations: Organize files/folders - - Integration: Calendar, reminders, messages - - Data: Interact with spreadsheets and documents - - Can be combined with screenshot tool for visual task assistance. - "#}, - _ => indoc! {r#" - Control the computer using Linux system automation. - - Features available: - - Shell scripting for system control - - X11/Wayland window management - - D-Bus for system services - - File and system management - - Desktop environment control (GNOME, KDE, etc.) - - Process management and monitoring - - System settings and configurations - - Can be combined with screenshot tool for visual task assistance. - "#}, - }; - - let computer_control_tool = Tool::new( - "computer_control", - computer_control_desc.to_string(), - object!({ - "type": "object", - "required": ["script"], - "properties": { - "script": { - "type": "string", - "description": "The automation script content (PowerShell for Windows, AppleScript for macOS)" - }, - "save_output": { - "type": "boolean", - "default": false, - "description": "Whether to save the script output to a file" - } - } - }), - ); - - let quick_script_desc = match std::env::consts::OS { - "windows" => indoc! {r#" - Create and run small PowerShell or Batch scripts for automation tasks. - PowerShell is recommended for most tasks. - - The script is saved to a temporary file and executed. - Some examples: - - Sort unique lines: Get-Content file.txt | Sort-Object -Unique - - Extract CSV column: Import-Csv file.csv | Select-Object -ExpandProperty Column2 - - Find text: Select-String -Pattern "pattern" -Path file.txt - "#}, - _ => indoc! {r#" - Create and run small scripts for automation tasks. - Supports Shell and Ruby (on macOS). - - The script is saved to a temporary file and executed. - Consider using shell script (bash) for most simple tasks first. - Ruby is useful for text processing or when you need more sophisticated scripting capabilities. - Some examples of shell: - - create a sorted list of unique lines: sort file.txt | uniq - - extract 2nd column in csv: awk -F "," '{ print $2}' - - pattern matching: grep pattern file.txt - "#}, - }; - - let quick_script_tool = Tool::new( - "automation_script", - quick_script_desc.to_string(), - object!({ - "type": "object", - "required": ["language", "script"], - "properties": { - "language": { - "type": "string", - "enum": ["shell", "ruby", "powershell", "batch"], - "description": "The scripting language to use" - }, - "script": { - "type": "string", - "description": "The script content" - }, - "save_output": { - "type": "boolean", - "default": false, - "description": "Whether to save the script output to a file" - } - } - }), - ); - - let cache_tool = Tool::new( - "cache", - indoc! {r#" - Manage cached files and data: - - list: List all cached files - - view: View content of a cached file - - delete: Delete a cached file - - clear: Clear all cached files - "#}, - object!({ - "type": "object", - "required": ["command"], - "properties": { - "command": { - "type": "string", - "enum": ["list", "view", "delete", "clear"], - "description": "The command to perform" - }, - "path": { - "type": "string", - "description": "Path to the cached file for view/delete commands" - } - } - }), - ); - - let pdf_tool = Tool::new( - "pdf_tool", - indoc! {r#" - Process PDF files to extract text and images. - Supports operations: - - extract_text: Extract all text content from the PDF - - extract_images: Extract and save embedded images to PNG files - - Use this when there is a .pdf file or files that need to be processed. - "#}, - object!({ - "type": "object", - "required": ["path", "operation"], - "properties": { - "path": { - "type": "string", - "description": "Path to the PDF file" - }, - "operation": { - "type": "string", - "enum": ["extract_text", "extract_images"], - "description": "Operation to perform on the PDF" - } - } - }), - ) - .annotate(ToolAnnotations { - title: Some("PDF process".to_string()), - read_only_hint: Some(true), - destructive_hint: Some(false), - idempotent_hint: Some(true), - open_world_hint: Some(false), - }); - - let docx_tool = Tool::new( - "docx_tool", - indoc! {r#" - Process DOCX files to extract text and create/update documents. - Supports operations: - - extract_text: Extract all text content and structure (headings, TOC) from the DOCX - - update_doc: Create a new DOCX or update existing one with provided content - Modes: - - append: Add content to end of document (default) - - replace: Replace specific text with new content - - structured: Add content with specific heading level and styling - - add_image: Add an image to the document (with optional caption) - - Use this when there is a .docx file that needs to be processed or created. - "#}, - object!({ - "type": "object", - "required": ["path", "operation"], - "properties": { - "path": { - "type": "string", - "description": "Path to the DOCX file" - }, - "operation": { - "type": "string", - "enum": ["extract_text", "update_doc"], - "description": "Operation to perform on the DOCX" - }, - "content": { - "type": "string", - "description": "Content to write (required for update_doc operation)" - }, - "params": { - "type": "object", - "description": "Additional parameters for update_doc operation", - "properties": { - "mode": { - "type": "string", - "enum": ["append", "replace", "structured", "add_image"], - "description": "Update mode (default: append)" - }, - "old_text": { - "type": "string", - "description": "Text to replace (required for replace mode)" - }, - "level": { - "type": "string", - "description": "Heading level for structured mode (e.g., 'Heading1', 'Heading2')" - }, - "image_path": { - "type": "string", - "description": "Path to the image file (required for add_image mode)" - }, - "width": { - "type": "integer", - "description": "Image width in pixels (optional)" - }, - "height": { - "type": "integer", - "description": "Image height in pixels (optional)" - }, - "style": { - "type": "object", - "description": "Styling options for the text", - "properties": { - "bold": { - "type": "boolean", - "description": "Make text bold" - }, - "italic": { - "type": "boolean", - "description": "Make text italic" - }, - "underline": { - "type": "boolean", - "description": "Make text underlined" - }, - "size": { - "type": "integer", - "description": "Font size in points" - }, - "color": { - "type": "string", - "description": "Text color in hex format (e.g., 'FF0000' for red)" - }, - "alignment": { - "type": "string", - "enum": ["left", "center", "right", "justified"], - "description": "Text alignment" - } - } - } - } - } - } - }), - ); - - let xlsx_tool = Tool::new( - "xlsx_tool", - indoc! {r#" - Process Excel (XLSX) files to read and manipulate spreadsheet data. - Supports operations: - - list_worksheets: List all worksheets in the workbook (returns name, index, column_count, row_count) - - get_columns: Get column names from a worksheet (returns values from the first row) - - get_range: Get values and formulas from a cell range (e.g., "A1:C10") (returns a 2D array organized as [row][column]) - - find_text: Search for text in a worksheet (returns a list of (row, column) coordinates) - - update_cell: Update a single cell's value (returns confirmation message) - - get_cell: Get value and formula from a specific cell (returns both value and formula if present) - - save: Save changes back to the file (returns confirmation message) - - Use this when working with Excel spreadsheets to analyze or modify data. - "#}, - object!({ - "type": "object", - "required": ["path", "operation"], - "properties": { - "path": { - "type": "string", - "description": "Path to the XLSX file" - }, - "operation": { - "type": "string", - "enum": ["list_worksheets", "get_columns", "get_range", "find_text", "update_cell", "get_cell", "save"], - "description": "Operation to perform on the XLSX file" - }, - "worksheet": { - "type": "string", - "description": "Worksheet name (if not provided, uses first worksheet)" - }, - "range": { - "type": "string", - "description": "Cell range in A1 notation (e.g., 'A1:C10') for get_range operation" - }, - "search_text": { - "type": "string", - "description": "Text to search for in find_text operation" - }, - "case_sensitive": { - "type": "boolean", - "default": false, - "description": "Whether search should be case-sensitive" - }, - "row": { - "type": "integer", - "description": "Row number for update_cell and get_cell operations" - }, - "col": { - "type": "integer", - "description": "Column number for update_cell and get_cell operations" - }, - "value": { - "type": "string", - "description": "New value for update_cell operation" - } - } - }), - ); - // choose_app_strategy().cache_dir() // - macOS/Linux: ~/.cache/goose/computer_controller/ // - Windows: ~\AppData\Local\Block\goose\cache\computer_controller\ @@ -540,19 +414,11 @@ impl ComputerControllerRouter { }; Self { - tools: vec![ - web_scrape_tool, - quick_script_tool, - computer_control_tool, - cache_tool, - pdf_tool, - docx_tool, - xlsx_tool, - ], + tool_router: Self::tool_router(), cache_dir, active_resources: Arc::new(Mutex::new(HashMap::new())), http_client: Client::builder().user_agent("Goose/1.0").build().unwrap(), - instructions: instructions.clone(), + instructions, system_automation, } } @@ -572,10 +438,12 @@ impl ComputerControllerRouter { extension: &str, ) -> Result { let cache_path = self.get_cache_path(prefix, extension); - fs::write(&cache_path, content).map_err(|e| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(format!("Failed to write to cache: {}", e)), - data: None, + fs::write(&cache_path, content).map_err(|e| { + ErrorData::new( + ErrorCode::INTERNAL_ERROR, + format!("Failed to write to cache: {}", e), + None, + ) })?; Ok(cache_path) } @@ -583,103 +451,103 @@ impl ComputerControllerRouter { // Helper function to register a file as a resource fn register_as_resource(&self, cache_path: &PathBuf, mime_type: &str) -> Result<(), ErrorData> { let uri = Url::from_file_path(cache_path) - .map_err(|_| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from("Invalid cache path"), - data: None, + .map_err(|_| { + ErrorData::new( + ErrorCode::INTERNAL_ERROR, + "Invalid cache path".to_string(), + None, + ) })? .to_string(); - let mut resource = RawResource::new(uri.clone(), cache_path.to_string_lossy().into_owned()); - resource.mime_type = Some(if mime_type == "blob" { - "blob".to_string() - } else { - "text".to_string() - }); - self.active_resources - .lock() - .unwrap() - .insert(uri, resource.no_annotation()); + let resource = ResourceContents::TextResourceContents { + uri: uri.clone(), + text: String::new(), // We'll read it when needed + mime_type: Some(mime_type.to_string()), + meta: None, + }; + + self.active_resources.lock().unwrap().insert(uri, resource); Ok(()) } - async fn web_scrape(&self, params: Value) -> Result, ErrorData> { - let url = params - .get("url") - .and_then(|v| v.as_str()) - .ok_or_else(|| ErrorData { - code: ErrorCode::INVALID_PARAMS, - message: Cow::from("Missing 'url' parameter"), - data: None, - })?; - - let save_as = params - .get("save_as") - .and_then(|v| v.as_str()) - .unwrap_or("text"); + /// Fetch and save content from a web page + #[tool( + name = "web_scrape", + description = " + Fetch and save content from a web page. The content can be saved as: + - text (for HTML pages) + - json (for API responses) + - binary (for images and other files) + The content is cached locally and can be accessed later using the cache_path + returned in the response. + " + )] + pub async fn web_scrape( + &self, + params: Parameters, + ) -> Result { + let params = params.0; + let url = ¶ms.url; + let save_as = params.save_as; // Fetch the content - let response = self - .http_client - .get(url) - .send() - .await - .map_err(|e| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(format!("Failed to fetch URL: {}", e)), - data: None, - })?; + let response = self.http_client.get(url).send().await.map_err(|e| { + ErrorData::new( + ErrorCode::INTERNAL_ERROR, + format!("Failed to fetch URL: {}", e), + None, + ) + })?; let status = response.status(); if !status.is_success() { - return Err(ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(format!("HTTP request failed with status: {}", status)), - data: None, - }); + return Err(ErrorData::new( + ErrorCode::INTERNAL_ERROR, + format!("HTTP request failed with status: {}", status), + None, + )); } // Process based on save_as parameter - let (content, extension) = match save_as { - "text" => { - let text = response.text().await.map_err(|e| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(format!("Failed to get text: {}", e)), - data: None, + let (content, extension, mime_type) = match save_as { + SaveAsFormat::Text => { + let text = response.text().await.map_err(|e| { + ErrorData::new( + ErrorCode::INTERNAL_ERROR, + format!("Failed to get text: {}", e), + None, + ) })?; - (text.into_bytes(), "txt") + (text.into_bytes(), "txt", "text/plain") } - "json" => { - let text = response.text().await.map_err(|e| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(format!("Failed to get text: {}", e)), - data: None, + SaveAsFormat::Json => { + let text = response.text().await.map_err(|e| { + ErrorData::new( + ErrorCode::INTERNAL_ERROR, + format!("Failed to get text: {}", e), + None, + ) })?; // Verify it's valid JSON - serde_json::from_str::(&text).map_err(|e| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(format!("Invalid JSON response: {}", e)), - data: None, + serde_json::from_str::(&text).map_err(|e| { + ErrorData::new( + ErrorCode::INTERNAL_ERROR, + format!("Invalid JSON response: {}", e), + None, + ) })?; - (text.into_bytes(), "json") + (text.into_bytes(), "json", "application/json") } - "binary" => { - let bytes = response.bytes().await.map_err(|e| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(format!("Failed to get bytes: {}", e)), - data: None, + SaveAsFormat::Binary => { + let bytes = response.bytes().await.map_err(|e| { + ErrorData::new( + ErrorCode::INTERNAL_ERROR, + format!("Failed to get bytes: {}", e), + None, + ) })?; - (bytes.to_vec(), "bin") - } - _ => { - return Err(ErrorData { - code: ErrorCode::INVALID_PARAMS, - message: Cow::from(format!( - "Invalid 'save_as' parameter: {}. Valid options are: 'text', 'json', 'binary'", - save_as - )), - data: None, - }); + (bytes.to_vec(), "bin", "application/octet-stream") } }; @@ -687,112 +555,148 @@ impl ComputerControllerRouter { let cache_path = self.save_to_cache(&content, "web", extension).await?; // Register as a resource - self.register_as_resource(&cache_path, save_as)?; + self.register_as_resource(&cache_path, mime_type)?; - Ok(vec![Content::text(format!( + Ok(CallToolResult::success(vec![Content::text(format!( "Content saved to: {}", cache_path.display() - ))]) + ))])) } - // Implement quick_script tool functionality - async fn quick_script(&self, params: Value) -> Result, ErrorData> { - let language = params - .get("language") - .and_then(|v| v.as_str()) - .ok_or_else(|| ErrorData { - code: ErrorCode::INVALID_PARAMS, - message: Cow::from("Missing 'language' parameter"), - data: None, - })?; + /// Create and run small scripts for automation tasks + #[cfg(target_os = "windows")] + #[tool( + name = "automation_script", + description = " + Create and run small PowerShell or Batch scripts for automation tasks. + PowerShell is recommended for most tasks. - let script = params - .get("script") - .and_then(|v| v.as_str()) - .ok_or_else(|| ErrorData { - code: ErrorCode::INVALID_PARAMS, - message: Cow::from("Missing 'script' parameter"), - data: None, - })?; + The script is saved to a temporary file and executed. + Some examples: + - Sort unique lines: Get-Content file.txt | Sort-Object -Unique + - Extract CSV column: Import-Csv file.csv | Select-Object -ExpandProperty Column2 + - Find text: Select-String -Pattern "pattern" -Path file.txt + " + )] + pub async fn automation_script( + &self, + params: Parameters, + ) -> Result { + self.automation_script_impl(params).await + } - let save_output = params - .get("save_output") - .and_then(|v| v.as_bool()) - .unwrap_or(false); + /// Create and run small scripts for automation tasks + #[cfg(not(target_os = "windows"))] + #[tool( + name = "automation_script", + description = " + Create and run small scripts for automation tasks. + Supports Shell and Ruby (on macOS). + + The script is saved to a temporary file and executed. + Consider using shell script (bash) for most simple tasks first. + Ruby is useful for text processing or when you need more sophisticated scripting capabilities. + Some examples of shell: + - create a sorted list of unique lines: sort file.txt | uniq + - extract 2nd column in csv: awk -F ',' '{ print $2}' + - pattern matching: grep pattern file.txt + " + )] + pub async fn automation_script( + &self, + params: Parameters, + ) -> Result { + self.automation_script_impl(params).await + } + + #[allow(clippy::too_many_lines)] + async fn automation_script_impl( + &self, + params: Parameters, + ) -> Result { + let params = params.0; + let language = params.language; + let script = ¶ms.script; + let save_output = params.save_output; // Create a temporary directory for the script - let script_dir = tempfile::tempdir().map_err(|e| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(format!("Failed to create temporary directory: {}", e)), - data: None, + let script_dir = tempfile::tempdir().map_err(|e| { + ErrorData::new( + ErrorCode::INTERNAL_ERROR, + format!("Failed to create temporary directory: {}", e), + None, + ) })?; let (shell, shell_arg) = self.system_automation.get_shell_command(); let command = match language { - "shell" | "batch" => { + ScriptLanguage::Shell | ScriptLanguage::Batch => { let script_path = script_dir.path().join(format!( "script.{}", if cfg!(windows) { "bat" } else { "sh" } )); - fs::write(&script_path, script).map_err(|e| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(format!("Failed to write script: {}", e)), - data: None, + fs::write(&script_path, script).map_err(|e| { + ErrorData::new( + ErrorCode::INTERNAL_ERROR, + format!("Failed to write script: {}", e), + None, + ) })?; // Set execute permissions on Unix systems #[cfg(unix)] { let mut perms = fs::metadata(&script_path) - .map_err(|e| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(format!("Failed to get file metadata: {}", e)), - data: None, + .map_err(|e| { + ErrorData::new( + ErrorCode::INTERNAL_ERROR, + format!("Failed to get file metadata: {}", e), + None, + ) })? .permissions(); perms.set_mode(0o755); // rwxr-xr-x - fs::set_permissions(&script_path, perms).map_err(|e| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(format!("Failed to set execute permissions: {}", e)), - data: None, + fs::set_permissions(&script_path, perms).map_err(|e| { + ErrorData::new( + ErrorCode::INTERNAL_ERROR, + format!("Failed to set execute permissions: {}", e), + None, + ) })?; } script_path.display().to_string() } - "ruby" => { + ScriptLanguage::Ruby => { let script_path = script_dir.path().join("script.rb"); - fs::write(&script_path, script).map_err(|e| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(format!("Failed to write script: {}", e)), - data: None, + fs::write(&script_path, script).map_err(|e| { + ErrorData::new( + ErrorCode::INTERNAL_ERROR, + format!("Failed to write script: {}", e), + None, + ) })?; format!("ruby {}", script_path.display()) } - "powershell" => { + ScriptLanguage::Powershell => { let script_path = script_dir.path().join("script.ps1"); - fs::write(&script_path, script).map_err(|e| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(format!("Failed to write script: {}", e)), - data: None, + fs::write(&script_path, script).map_err(|e| { + ErrorData::new( + ErrorCode::INTERNAL_ERROR, + format!("Failed to write script: {}", e), + None, + ) })?; script_path.display().to_string() } - _ => { - return Err(ErrorData { - code: ErrorCode::INVALID_PARAMS, - message: Cow::from(format!("Invalid 'language' parameter: {}. Valid options are: 'shell', 'batch', 'ruby', 'powershell'", language)), - data: None, - }); - } }; // Run the script let output = match language { - "powershell" => { + ScriptLanguage::Powershell => { // For PowerShell, we need to use -File instead of -Command Command::new("powershell") .arg("-NoProfile") @@ -802,10 +706,12 @@ impl ComputerControllerRouter { .env("GOOSE_TERMINAL", "1") .output() .await - .map_err(|e| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(format!("Failed to run script: {}", e)), - data: None, + .map_err(|e| { + ErrorData::new( + ErrorCode::INTERNAL_ERROR, + format!("Failed to run script: {}", e), + None, + ) })? } _ => Command::new(shell) @@ -814,10 +720,12 @@ impl ComputerControllerRouter { .env("GOOSE_TERMINAL", "1") .output() .await - .map_err(|e| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(format!("Failed to run script: {}", e)), - data: None, + .map_err(|e| { + ErrorData::new( + ErrorCode::INTERNAL_ERROR, + format!("Failed to run script: {}", e), + None, + ) })?, }; @@ -844,33 +752,118 @@ impl ComputerControllerRouter { self.register_as_resource(&cache_path, "text")?; } - Ok(vec![Content::text(result)]) + Ok(CallToolResult::success(vec![Content::text(result)])) } - // Implement computer control functionality - async fn computer_control(&self, params: Value) -> Result, ErrorData> { - let script = params - .get("script") - .and_then(|v| v.as_str()) - .ok_or_else(|| ErrorData { - code: ErrorCode::INVALID_PARAMS, - message: Cow::from("Missing 'script' parameter"), - data: None, - })?; + /// Control the computer using system automation + #[cfg(target_os = "windows")] + #[tool( + name = "computer_control", + description = " + Control the computer using Windows system automation. - let save_output = params - .get("save_output") - .and_then(|v| v.as_bool()) - .unwrap_or(false); + Features available: + - PowerShell automation for system control + - UI automation through PowerShell + - File and system management + - Windows-specific features and settings + + Can be combined with screenshot tool for visual task assistance. + " + )] + pub async fn computer_control( + &self, + params: Parameters, + ) -> Result { + self.computer_control_impl(params).await + } + + /// Control the computer using system automation + #[cfg(target_os = "macos")] + #[tool( + name = "computer_control", + description = " + Control the computer using AppleScript (macOS only). Automate applications and system features. + + Key capabilities: + - Control Applications: Launch, quit, manage apps (Mail, Safari, iTunes, etc) + - Interact with app-specific feature: (e.g, edit documents, process photos) + - Perform tasks in third-party apps that support AppleScript + - UI Automation: Simulate user interactions like, clicking buttons, select menus, type text, filling out forms + - System Control: Manage settings (volume, brightness, wifi), shutdown/restart, monitor events + - Web & Email: Open URLs, web automation, send/organize emails, handle attachments + - Media: Manage music libraries, photo collections, playlists + - File Operations: Organize files/folders + - Integration: Calendar, reminders, messages + - Data: Interact with spreadsheets and documents + + Can be combined with screenshot tool for visual task assistance. + " + )] + pub async fn computer_control( + &self, + params: Parameters, + ) -> Result { + self.computer_control_impl(params).await + } + + /// Control the computer using system automation + #[cfg(target_os = "linux")] + #[tool( + name = "computer_control", + description = " + Control the computer using Linux system automation. + + Features available: + - Shell scripting for system control + - X11/Wayland window management + - D-Bus for system services + - File and system management + - Desktop environment control (GNOME, KDE, etc.) + - Process management and monitoring + - System settings and configurations + + Can be combined with screenshot tool for visual task assistance. + " + )] + pub async fn computer_control( + &self, + params: Parameters, + ) -> Result { + self.computer_control_impl(params).await + } + + /// Control the computer using system automation (fallback for other OS) + #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))] + #[tool( + name = "computer_control", + description = "Control the computer using system automation. Features available depend on your operating system. Can be combined with screenshot tool for visual task assistance." + )] + pub async fn computer_control( + &self, + params: Parameters, + ) -> Result { + self.computer_control_impl(params).await + } + + async fn computer_control_impl( + &self, + params: Parameters, + ) -> Result { + let params = params.0; + let script = ¶ms.script; + let save_output = params.save_output; // Use platform-specific automation let output = self .system_automation .execute_system_script(script) - .map_err(|e| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(format!("Failed to execute script: {}", e)), - data: None, + .map_err(|e| { + ErrorData::new( + ErrorCode::INTERNAL_ERROR, + format!("Failed to execute script: {}", e), + None, + ) })?; let mut result = format!("Script completed successfully.\n\nOutput:\n{}", output); @@ -886,366 +879,372 @@ impl ComputerControllerRouter { self.register_as_resource(&cache_path, "text")?; } - Ok(vec![Content::text(result)]) + Ok(CallToolResult::success(vec![Content::text(result)])) } - async fn xlsx_tool(&self, params: Value) -> Result, ErrorData> { - let path = params - .get("path") - .and_then(|v| v.as_str()) - .ok_or_else(|| ErrorData { - code: ErrorCode::INVALID_PARAMS, - message: Cow::from("Missing 'path' parameter"), - data: None, - })?; + /// Process Excel (XLSX) files to read and manipulate spreadsheet data + #[tool( + name = "xlsx_tool", + description = " + Process Excel (XLSX) files to read and manipulate spreadsheet data. + Supports operations: + - list_worksheets: List all worksheets in the workbook (returns name, index, column_count, row_count) + - get_columns: Get column names from a worksheet (returns values from the first row) + - get_range: Get values and formulas from a cell range (e.g., 'A1:C10') (returns a 2D array organized as [row][column]) + - find_text: Search for text in a worksheet (returns a list of (row, column) coordinates) + - update_cell: Update a single cell's value (returns confirmation message) + - get_cell: Get value and formula from a specific cell (returns both value and formula if present) + - save: Save changes back to the file (returns confirmation message) - let operation = params - .get("operation") - .and_then(|v| v.as_str()) - .ok_or_else(|| ErrorData { - code: ErrorCode::INVALID_PARAMS, - message: Cow::from("Missing 'operation' parameter"), - data: None, - })?; + Use this when working with Excel spreadsheets to analyze or modify data. + " + )] + pub async fn xlsx_tool( + &self, + params: Parameters, + ) -> Result { + let params = params.0; + let path = ¶ms.path; + let operation = params.operation; match operation { - "list_worksheets" => { - let xlsx = xlsx_tool::XlsxTool::new(path).map_err(|e| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(e.to_string()), - data: None, - })?; - let worksheets = xlsx.list_worksheets().map_err(|e| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(e.to_string()), - data: None, - })?; - Ok(vec![Content::text(format!("{:#?}", worksheets))]) + XlsxOperation::ListWorksheets => { + let xlsx = xlsx_tool::XlsxTool::new(path) + .map_err(|e| ErrorData::new(ErrorCode::INTERNAL_ERROR, e.to_string(), None))?; + let worksheets = xlsx + .list_worksheets() + .map_err(|e| ErrorData::new(ErrorCode::INTERNAL_ERROR, e.to_string(), None))?; + Ok(CallToolResult::success(vec![Content::text(format!( + "{:#?}", + worksheets + ))])) } - "get_columns" => { - let xlsx = xlsx_tool::XlsxTool::new(path).map_err(|e| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(e.to_string()), - data: None, - })?; - let worksheet = if let Some(name) = params.get("worksheet").and_then(|v| v.as_str()) - { - xlsx.get_worksheet_by_name(name).map_err(|e| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(e.to_string()), - data: None, + XlsxOperation::GetColumns => { + let xlsx = xlsx_tool::XlsxTool::new(path) + .map_err(|e| ErrorData::new(ErrorCode::INTERNAL_ERROR, e.to_string(), None))?; + let worksheet = if let Some(name) = ¶ms.worksheet { + xlsx.get_worksheet_by_name(name).map_err(|e| { + ErrorData::new(ErrorCode::INTERNAL_ERROR, e.to_string(), None) })? } else { - xlsx.get_worksheet_by_index(0).map_err(|e| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(e.to_string()), - data: None, + xlsx.get_worksheet_by_index(0).map_err(|e| { + ErrorData::new(ErrorCode::INTERNAL_ERROR, e.to_string(), None) })? }; - let columns = xlsx.get_column_names(worksheet).map_err(|e| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(e.to_string()), - data: None, - })?; - Ok(vec![Content::text(format!("{:#?}", columns))]) + let columns = xlsx + .get_column_names(worksheet) + .map_err(|e| ErrorData::new(ErrorCode::INTERNAL_ERROR, e.to_string(), None))?; + Ok(CallToolResult::success(vec![Content::text(format!( + "{:#?}", + columns + ))])) } - "get_range" => { - let range = params - .get("range") - .and_then(|v| v.as_str()) - .ok_or_else(|| ErrorData { - code: ErrorCode::INVALID_PARAMS, - message: Cow::from("Missing 'range' parameter"), - data: None, - })?; - - let xlsx = xlsx_tool::XlsxTool::new(path).map_err(|e| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(e.to_string()), - data: None, + XlsxOperation::GetRange => { + let range = params.range.as_ref().ok_or_else(|| { + ErrorData::new( + ErrorCode::INVALID_PARAMS, + "Missing 'range' parameter".to_string(), + None, + ) })?; - let worksheet = if let Some(name) = params.get("worksheet").and_then(|v| v.as_str()) - { - xlsx.get_worksheet_by_name(name).map_err(|e| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(e.to_string()), - data: None, + + let xlsx = xlsx_tool::XlsxTool::new(path) + .map_err(|e| ErrorData::new(ErrorCode::INTERNAL_ERROR, e.to_string(), None))?; + let worksheet = if let Some(name) = ¶ms.worksheet { + xlsx.get_worksheet_by_name(name).map_err(|e| { + ErrorData::new(ErrorCode::INTERNAL_ERROR, e.to_string(), None) })? } else { - xlsx.get_worksheet_by_index(0).map_err(|e| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(e.to_string()), - data: None, + xlsx.get_worksheet_by_index(0).map_err(|e| { + ErrorData::new(ErrorCode::INTERNAL_ERROR, e.to_string(), None) })? }; - let range_data = xlsx.get_range(worksheet, range).map_err(|e| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(e.to_string()), - data: None, - })?; - Ok(vec![Content::text(format!("{:#?}", range_data))]) + let range_data = xlsx + .get_range(worksheet, range) + .map_err(|e| ErrorData::new(ErrorCode::INTERNAL_ERROR, e.to_string(), None))?; + Ok(CallToolResult::success(vec![Content::text(format!( + "{:#?}", + range_data + ))])) } - "find_text" => { - let search_text = params - .get("search_text") - .and_then(|v| v.as_str()) - .ok_or_else(|| ErrorData { - code: ErrorCode::INVALID_PARAMS, - message: Cow::from("Missing 'search_text' parameter"), - data: None, - })?; - - let case_sensitive = params - .get("case_sensitive") - .and_then(|v| v.as_bool()) - .unwrap_or(false); - - let xlsx = xlsx_tool::XlsxTool::new(path).map_err(|e| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(e.to_string()), - data: None, + XlsxOperation::FindText => { + let search_text = params.search_text.as_ref().ok_or_else(|| { + ErrorData::new( + ErrorCode::INVALID_PARAMS, + "Missing 'search_text' parameter".to_string(), + None, + ) })?; - let worksheet = if let Some(name) = params.get("worksheet").and_then(|v| v.as_str()) - { - xlsx.get_worksheet_by_name(name).map_err(|e| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(e.to_string()), - data: None, + + let case_sensitive = params.case_sensitive; + + let xlsx = xlsx_tool::XlsxTool::new(path) + .map_err(|e| ErrorData::new(ErrorCode::INTERNAL_ERROR, e.to_string(), None))?; + let worksheet = if let Some(name) = ¶ms.worksheet { + xlsx.get_worksheet_by_name(name).map_err(|e| { + ErrorData::new(ErrorCode::INTERNAL_ERROR, e.to_string(), None) })? } else { - xlsx.get_worksheet_by_index(0).map_err(|e| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(e.to_string()), - data: None, + xlsx.get_worksheet_by_index(0).map_err(|e| { + ErrorData::new(ErrorCode::INTERNAL_ERROR, e.to_string(), None) })? }; let matches = xlsx .find_in_worksheet(worksheet, search_text, case_sensitive) - .map_err(|e| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(e.to_string()), - data: None, - })?; - Ok(vec![Content::text(format!( + .map_err(|e| ErrorData::new(ErrorCode::INTERNAL_ERROR, e.to_string(), None))?; + Ok(CallToolResult::success(vec![Content::text(format!( "Found matches at: {:#?}", matches - ))]) + ))])) } - "update_cell" => { - let row = require_u64_parameter(¶ms, "row")?; - let col = require_u64_parameter(¶ms, "col")?; - let value = require_str_parameter(¶ms, "value")?; - - let worksheet_name = params - .get("worksheet") - .and_then(|v| v.as_str()) - .unwrap_or("Sheet1"); - - let mut xlsx = xlsx_tool::XlsxTool::new(path).map_err(|e| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(e.to_string()), - data: None, + XlsxOperation::UpdateCell => { + let row = params.row.ok_or_else(|| { + ErrorData::new( + ErrorCode::INVALID_PARAMS, + "Missing 'row' parameter".to_string(), + None, + ) })?; + let col = params.col.ok_or_else(|| { + ErrorData::new( + ErrorCode::INVALID_PARAMS, + "Missing 'col' parameter".to_string(), + None, + ) + })?; + let value = params.value.as_ref().ok_or_else(|| { + ErrorData::new( + ErrorCode::INVALID_PARAMS, + "Missing 'value' parameter".to_string(), + None, + ) + })?; + + let worksheet_name = params.worksheet.as_deref().unwrap_or("Sheet1"); + + let mut xlsx = xlsx_tool::XlsxTool::new(path) + .map_err(|e| ErrorData::new(ErrorCode::INTERNAL_ERROR, e.to_string(), None))?; xlsx.update_cell(worksheet_name, row as u32, col as u32, value) - .map_err(|e| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(e.to_string()), - data: None, - })?; - xlsx.save(path).map_err(|e| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(e.to_string()), - data: None, - })?; - Ok(vec![Content::text(format!( + .map_err(|e| ErrorData::new(ErrorCode::INTERNAL_ERROR, e.to_string(), None))?; + xlsx.save(path) + .map_err(|e| ErrorData::new(ErrorCode::INTERNAL_ERROR, e.to_string(), None))?; + Ok(CallToolResult::success(vec![Content::text(format!( "Updated cell ({}, {}) to '{}' in worksheet '{}'", row, col, value, worksheet_name - ))]) + ))])) } - "save" => { - let xlsx = xlsx_tool::XlsxTool::new(path).map_err(|e| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(e.to_string()), - data: None, - })?; - xlsx.save(path).map_err(|e| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(e.to_string()), - data: None, - })?; - Ok(vec![Content::text("File saved successfully.")]) + XlsxOperation::Save => { + let xlsx = xlsx_tool::XlsxTool::new(path) + .map_err(|e| ErrorData::new(ErrorCode::INTERNAL_ERROR, e.to_string(), None))?; + xlsx.save(path) + .map_err(|e| ErrorData::new(ErrorCode::INTERNAL_ERROR, e.to_string(), None))?; + Ok(CallToolResult::success(vec![Content::text( + "File saved successfully.", + )])) } - "get_cell" => { - let row = params - .get("row") - .and_then(|v| v.as_u64()) - .ok_or_else(|| ErrorData { - code: ErrorCode::INVALID_PARAMS, - message: Cow::from("Missing 'row' parameter"), - data: None, - })?; - - let col = params - .get("col") - .and_then(|v| v.as_u64()) - .ok_or_else(|| ErrorData { - code: ErrorCode::INVALID_PARAMS, - message: Cow::from("Missing 'col' parameter"), - data: None, - })?; - - let xlsx = xlsx_tool::XlsxTool::new(path).map_err(|e| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(e.to_string()), - data: None, + XlsxOperation::GetCell => { + let row = params.row.ok_or_else(|| { + ErrorData::new( + ErrorCode::INVALID_PARAMS, + "Missing 'row' parameter".to_string(), + None, + ) })?; - let worksheet = if let Some(name) = params.get("worksheet").and_then(|v| v.as_str()) - { - xlsx.get_worksheet_by_name(name).map_err(|e| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(e.to_string()), - data: None, + + let col = params.col.ok_or_else(|| { + ErrorData::new( + ErrorCode::INVALID_PARAMS, + "Missing 'col' parameter".to_string(), + None, + ) + })?; + + let xlsx = xlsx_tool::XlsxTool::new(path) + .map_err(|e| ErrorData::new(ErrorCode::INTERNAL_ERROR, e.to_string(), None))?; + let worksheet = if let Some(name) = ¶ms.worksheet { + xlsx.get_worksheet_by_name(name).map_err(|e| { + ErrorData::new(ErrorCode::INTERNAL_ERROR, e.to_string(), None) })? } else { - xlsx.get_worksheet_by_index(0).map_err(|e| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(e.to_string()), - data: None, + xlsx.get_worksheet_by_index(0).map_err(|e| { + ErrorData::new(ErrorCode::INTERNAL_ERROR, e.to_string(), None) })? }; let cell_value = xlsx .get_cell_value(worksheet, row as u32, col as u32) - .map_err(|e| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(e.to_string()), - data: None, - })?; - Ok(vec![Content::text(format!("{:#?}", cell_value))]) + .map_err(|e| ErrorData::new(ErrorCode::INTERNAL_ERROR, e.to_string(), None))?; + Ok(CallToolResult::success(vec![Content::text(format!( + "{:#?}", + cell_value + ))])) } - _ => Err(ErrorData { - code: ErrorCode::INVALID_PARAMS, - message: Cow::from(format!("Invalid operation: {}", operation)), - data: None, - }), } } - // Implement cache tool functionality - async fn docx_tool(&self, params: Value) -> Result, ErrorData> { - let path = params - .get("path") - .and_then(|v| v.as_str()) - .ok_or_else(|| ErrorData { - code: ErrorCode::INVALID_PARAMS, - message: Cow::from("Missing 'path' parameter"), - data: None, - })?; + /// Process DOCX files to extract text and create/update documents + #[tool( + name = "docx_tool", + description = " + Process DOCX files to extract text and create/update documents. + Supports operations: + - extract_text: Extract all text content and structure (headings, TOC) from the DOCX + - update_doc: Create a new DOCX or update existing one with provided content + Modes: + - append: Add content to end of document (default) + - replace: Replace specific text with new content + - structured: Add content with specific heading level and styling + - add_image: Add an image to the document (with optional caption) - let operation = params - .get("operation") - .and_then(|v| v.as_str()) - .ok_or_else(|| ErrorData { - code: ErrorCode::INVALID_PARAMS, - message: Cow::from("Missing 'operation' parameter"), - data: None, - })?; + Use this when there is a .docx file that needs to be processed or created. + " + )] + pub async fn docx_tool( + &self, + params: Parameters, + ) -> Result { + let params = params.0; + let path = ¶ms.path; + let operation = params.operation; - crate::computercontroller::docx_tool::docx_tool( + // Convert enum to string for the existing implementation + let operation_str = match operation { + DocxOperation::ExtractText => "extract_text", + DocxOperation::UpdateDoc => "update_doc", + }; + + // Convert typed params back to JSON for the internal docx_tool impl + let json_params = params + .params + .as_ref() + .map(|p| serde_json::to_value(p).unwrap_or_else(|_| serde_json::Value::Null)); + + let result = crate::computercontroller::docx_tool::docx_tool( path, - operation, - params.get("content").and_then(|v| v.as_str()), - params.get("params"), + operation_str, + params.content.as_deref(), + json_params.as_ref(), ) .await + .map_err(|e| ErrorData::new(e.code, e.message, e.data))?; + + Ok(CallToolResult::success(result)) } - async fn pdf_tool(&self, params: Value) -> Result, ErrorData> { - let path = params - .get("path") - .and_then(|v| v.as_str()) - .ok_or_else(|| ErrorData { - code: ErrorCode::INVALID_PARAMS, - message: Cow::from("Missing 'path' parameter"), - data: None, - })?; + /// Process PDF files to extract text and images + #[tool( + name = "pdf_tool", + description = " + Process PDF files to extract text and images. + Supports operations: + - extract_text: Extract all text content from the PDF + - extract_images: Extract and save embedded images to PNG files - let operation = params - .get("operation") - .and_then(|v| v.as_str()) - .ok_or_else(|| ErrorData { - code: ErrorCode::INVALID_PARAMS, - message: Cow::from("Missing 'operation' parameter"), - data: None, - })?; + Use this when there is a .pdf file or files that need to be processed. + " + )] + pub async fn pdf_tool( + &self, + params: Parameters, + ) -> Result { + let params = params.0; + let path = ¶ms.path; + let operation = params.operation; - crate::computercontroller::pdf_tool::pdf_tool(path, operation, &self.cache_dir).await + // Convert enum to string for the existing implementation + let operation_str = match operation { + PdfOperation::ExtractText => "extract_text", + PdfOperation::ExtractImages => "extract_images", + }; + + let result = + crate::computercontroller::pdf_tool::pdf_tool(path, operation_str, &self.cache_dir) + .await + .map_err(|e| ErrorData::new(e.code, e.message, e.data))?; + + Ok(CallToolResult::success(result)) } - async fn cache(&self, params: Value) -> Result, ErrorData> { - let command = params - .get("command") - .and_then(|v| v.as_str()) - .ok_or_else(|| ErrorData { - code: ErrorCode::INVALID_PARAMS, - message: Cow::from("Missing 'command' parameter"), - data: None, - })?; + /// Manage cached files and data + #[tool( + name = "cache", + description = " + Manage cached files and data: + - list: List all cached files + - view: View content of a cached file + - delete: Delete a cached file + - clear: Clear all cached files + " + )] + pub async fn cache( + &self, + params: Parameters, + ) -> Result { + let command = params.0.command; + let path = params.0.path.as_deref(); match command { - "list" => { + CacheCommand::List => { let mut files = Vec::new(); - for entry in fs::read_dir(&self.cache_dir).map_err(|e| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(format!("Failed to read cache directory: {}", e)), - data: None, + for entry in fs::read_dir(&self.cache_dir).map_err(|e| { + ErrorData::new( + ErrorCode::INTERNAL_ERROR, + format!("Failed to read cache directory: {}", e), + None, + ) })? { - let entry = entry.map_err(|e| ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(format!("Failed to read directory entry: {}", e)), - data: None, + let entry = entry.map_err(|e| { + ErrorData::new( + ErrorCode::INTERNAL_ERROR, + format!("Failed to read directory entry: {}", e), + None, + ) })?; files.push(format!("{}", entry.path().display())); } files.sort(); - Ok(vec![Content::text(format!( + Ok(CallToolResult::success(vec![Content::text(format!( "Cached files:\n{}", files.join("\n") - ))]) + ))])) } - "view" => { - let path = params.get("path").and_then(|v| v.as_str()).ok_or_else(|| - ErrorData { - code: ErrorCode::INVALID_PARAMS, - message: Cow::from("Missing 'path' parameter for view"), - data: None, - })?; + CacheCommand::View => { + let path = path.ok_or_else(|| { + ErrorData::new( + ErrorCode::INVALID_PARAMS, + "Missing 'path' parameter for view".to_string(), + None, + ) + })?; - let content = fs::read_to_string(path).map_err(|e| - ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(format!("Failed to read file: {}", e)), - data: None, - })?; + let content = fs::read_to_string(path).map_err(|e| { + ErrorData::new( + ErrorCode::INTERNAL_ERROR, + format!("Failed to read file: {}", e), + None, + ) + })?; - Ok(vec![Content::text(format!( + Ok(CallToolResult::success(vec![Content::text(format!( "Content of {}:\n\n{}", path, content - ))]) + ))])) } - "delete" => { - let path = params.get("path").and_then(|v| v.as_str()).ok_or_else(|| - ErrorData { - code: ErrorCode::INVALID_PARAMS, - message: Cow::from("Missing 'path' parameter for delete"), - data: None, - })?; + CacheCommand::Delete => { + let path = path.ok_or_else(|| { + ErrorData::new( + ErrorCode::INVALID_PARAMS, + "Missing 'path' parameter for delete".to_string(), + None, + ) + })?; - fs::remove_file(path).map_err(|e| - ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(format!("Failed to delete file: {}", e)), - data: None, - })?; + fs::remove_file(path).map_err(|e| { + ErrorData::new( + ErrorCode::INTERNAL_ERROR, + format!("Failed to delete file: {}", e), + None, + ) + })?; // Remove from active resources if present if let Ok(url) = Url::from_file_path(path) { @@ -1255,150 +1254,97 @@ impl ComputerControllerRouter { .remove(&url.to_string()); } - Ok(vec![Content::text(format!("Deleted file: {}", path))]) + Ok(CallToolResult::success(vec![Content::text(format!( + "Deleted file: {}", + path + ))])) } - "clear" => { - fs::remove_dir_all(&self.cache_dir).map_err(|e| - ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(format!("Failed to clear cache directory: {}", e)), - data: None, - })?; - fs::create_dir_all(&self.cache_dir).map_err(|e| - ErrorData { - code: ErrorCode::INTERNAL_ERROR, - message: Cow::from(format!("Failed to recreate cache directory: {}", e)), - data: None, - })?; + CacheCommand::Clear => { + fs::remove_dir_all(&self.cache_dir).map_err(|e| { + ErrorData::new( + ErrorCode::INTERNAL_ERROR, + format!("Failed to clear cache directory: {}", e), + None, + ) + })?; + fs::create_dir_all(&self.cache_dir).map_err(|e| { + ErrorData::new( + ErrorCode::INTERNAL_ERROR, + format!("Failed to recreate cache directory: {}", e), + None, + ) + })?; // Clear active resources self.active_resources.lock().unwrap().clear(); - Ok(vec![Content::text("Cache cleared successfully.")]) + Ok(CallToolResult::success(vec![Content::text( + "Cache cleared successfully.", + )])) } - _ => Err(ErrorData { - code: ErrorCode::INVALID_PARAMS, - message: Cow::from(format!( - "Invalid 'command' parameter: {}. Valid options are: 'list', 'view', 'delete', 'clear'", - command)), - data: None, - }), } } } -impl Router for ComputerControllerRouter { - fn name(&self) -> String { - "ComputerControllerExtension".to_string() +#[tool_handler(router = self.tool_router)] +impl ServerHandler for ComputerControllerServer { + fn get_info(&self) -> ServerInfo { + ServerInfo { + server_info: Implementation { + name: "goose-computercontroller".to_string(), + version: env!("CARGO_PKG_VERSION").to_owned(), + }, + capabilities: ServerCapabilities::builder() + .enable_tools() + .enable_resources() + .build(), + instructions: Some(self.instructions.clone()), + ..Default::default() + } } - fn instructions(&self) -> String { - self.instructions.clone() - } - - fn capabilities(&self) -> ServerCapabilities { - CapabilitiesBuilder::new() - .with_tools(false) - .with_resources(false, false) - .build() - } - - fn list_tools(&self) -> Vec { - self.tools.clone() - } - - fn call_tool( + async fn list_resources( &self, - tool_name: &str, - arguments: Value, - _notifier: mpsc::Sender, - ) -> Pin, ErrorData>> + Send + 'static>> { - let this = self.clone(); - let tool_name = tool_name.to_string(); - Box::pin(async move { - match tool_name.as_str() { - "web_scrape" => this.web_scrape(arguments).await, - "automation_script" => this.quick_script(arguments).await, - "computer_control" => this.computer_control(arguments).await, - "cache" => this.cache(arguments).await, - "pdf_tool" => this.pdf_tool(arguments).await, - "docx_tool" => this.docx_tool(arguments).await, - "xlsx_tool" => this.xlsx_tool(arguments).await, - _ => Err(ErrorData { - code: ErrorCode::INVALID_REQUEST, - message: Cow::from(format!("Tool {} not found", tool_name)), - data: None, - }), - } - }) - } - - fn list_resources(&self) -> Vec { + _pagination: Option, + _context: RequestContext, + ) -> Result { let active_resources = self.active_resources.lock().unwrap(); - let resources = active_resources.values().cloned().collect(); - tracing::info!("Listing resources: {:?}", resources); - resources - } - - fn read_resource( - &self, - uri: &str, - ) -> Pin> + Send + 'static>> { - let uri = uri.to_string(); - let this = self.clone(); - - Box::pin(async move { - let active_resources = this.active_resources.lock().unwrap(); - let resource = active_resources - .get(&uri) - .ok_or_else(|| ResourceError::NotFound(format!("Resource not found: {}", uri)))? - .clone(); - - let url = Url::parse(&uri) - .map_err(|e| ResourceError::NotFound(format!("Invalid URI: {}", e)))?; - - if url.scheme() != "file" { - return Err(ResourceError::NotFound( - "Only file:// URIs are supported".into(), - )); - } - - let path = url - .to_file_path() - .map_err(|_| ResourceError::NotFound("Invalid file path in URI".into()))?; - - match resource.raw.mime_type.as_deref() { - Some("text") | Some("json") | None => fs::read_to_string(&path).map_err(|e| { - ResourceError::ExecutionError(format!("Failed to read file: {}", e)) - }), - Some("binary") => { - let bytes = fs::read(&path).map_err(|e| { - ResourceError::ExecutionError(format!("Failed to read file: {}", e)) - })?; - Ok(base64::prelude::BASE64_STANDARD.encode(bytes)) - } - Some(mime_type) => Err(ResourceError::NotFound(format!( - "Unsupported mime type: {}", - mime_type - ))), - } + let resources: Vec = active_resources + .keys() + .map(|uri| Resource { + raw: RawResource { + name: uri.split('/').next_back().unwrap_or("").to_string(), + uri: uri.clone(), + description: None, + mime_type: None, + size: None, + }, + annotations: None, + }) + .collect(); + Ok(ListResourcesResult { + resources, + next_cursor: None, }) } - fn list_prompts(&self) -> Vec { - vec![] - } - - fn get_prompt( + async fn read_resource( &self, - prompt_name: &str, - ) -> Pin> + Send + 'static>> { - let prompt_name = prompt_name.to_string(); - Box::pin(async move { - Err(PromptError::NotFound(format!( - "Prompt {} not found", - prompt_name - ))) + params: ReadResourceRequestParam, + _context: RequestContext, + ) -> Result { + let active_resources = self.active_resources.lock().unwrap(); + let resource = active_resources.get(¶ms.uri).ok_or_else(|| { + ErrorData::new( + ErrorCode::INVALID_REQUEST, + format!("Resource not found: {}", params.uri), + None, + ) + })?; + + // Clone the resource to return + Ok(ReadResourceResult { + contents: vec![resource.clone()], }) } } diff --git a/crates/goose-mcp/src/lib.rs b/crates/goose-mcp/src/lib.rs index 2a369213ed..a096a1cea3 100644 --- a/crates/goose-mcp/src/lib.rs +++ b/crates/goose-mcp/src/lib.rs @@ -10,11 +10,12 @@ pub static APP_STRATEGY: Lazy = Lazy::new(|| AppStrategyArgs { pub mod autovisualiser; pub mod computercontroller; pub mod developer; +pub mod mcp_server_runner; mod memory; pub mod tutorial; pub use autovisualiser::AutoVisualiserRouter; -pub use computercontroller::ComputerControllerRouter; +pub use computercontroller::ComputerControllerServer; pub use developer::rmcp_developer::DeveloperServer; pub use memory::MemoryServer; pub use tutorial::TutorialServer; diff --git a/crates/goose-mcp/src/mcp_server_runner.rs b/crates/goose-mcp/src/mcp_server_runner.rs new file mode 100644 index 0000000000..d1f26327ef --- /dev/null +++ b/crates/goose-mcp/src/mcp_server_runner.rs @@ -0,0 +1,45 @@ +use crate::{ + AutoVisualiserRouter, ComputerControllerServer, DeveloperServer, MemoryServer, TutorialServer, +}; +use anyhow::{anyhow, Result}; +use rmcp::{transport::stdio, ServiceExt}; + +/// Run an MCP server by name +/// +/// This function handles the common logic for starting MCP servers. +/// The caller is responsible for setting up logging before calling this function. +pub async fn run_mcp_server(name: &str) -> Result<()> { + if name == "googledrive" || name == "google_drive" { + return Err(anyhow!( + "the built-in Google Drive extension has been removed" + )); + } + + tracing::info!("Starting MCP server"); + + match name { + "autovisualiser" => serve_and_wait(AutoVisualiserRouter::new()).await, + "computercontroller" => serve_and_wait(ComputerControllerServer::new()).await, + "developer" => serve_and_wait(DeveloperServer::new()).await, + "memory" => serve_and_wait(MemoryServer::new()).await, + "tutorial" => serve_and_wait(TutorialServer::new()).await, + _ => { + tracing::warn!("Unknown MCP server name: {}", name); + Err(anyhow!("Unknown MCP server name: {}", name)) + } + } +} + +/// Helper function to run any MCP server with common error handling +async fn serve_and_wait(server: S) -> Result<()> +where + S: rmcp::ServerHandler, +{ + let service = server.serve(stdio()).await.inspect_err(|e| { + tracing::error!("serving error: {:?}", e); + })?; + + service.waiting().await?; + + Ok(()) +} diff --git a/crates/goose-server/src/commands/mcp.rs b/crates/goose-server/src/commands/mcp.rs deleted file mode 100644 index eba1428c4d..0000000000 --- a/crates/goose-server/src/commands/mcp.rs +++ /dev/null @@ -1,77 +0,0 @@ -use anyhow::{anyhow, Result}; -use goose_mcp::{ - AutoVisualiserRouter, ComputerControllerRouter, DeveloperServer, MemoryServer, TutorialServer, -}; -use mcp_server::router::RouterService; -use mcp_server::{BoundedService, ByteTransport, Server}; -use rmcp::{transport::stdio, ServiceExt}; -use tokio::io::{stdin, stdout}; - -pub async fn run(name: &str) -> Result<()> { - crate::logging::setup_logging(Some(&format!("mcp-{name}")))?; - - if name == "googledrive" || name == "google_drive" { - return Err(anyhow!( - "the built-in Google Drive extension has been removed" - )); - } - - tracing::info!("Starting MCP server"); - - // Handle RMCP-based servers - if name == "developer" { - let service = DeveloperServer::new() - .serve(stdio()) - .await - .inspect_err(|e| { - tracing::error!("serving error: {:?}", e); - })?; - - service.waiting().await?; - return Ok(()); - } - - if name == "autovisualiser" { - let service = AutoVisualiserRouter::new() - .serve(stdio()) - .await - .inspect_err(|e| { - tracing::error!("serving error: {:?}", e); - })?; - - service.waiting().await?; - return Ok(()); - } - - if name == "tutorial" { - let service = TutorialServer::new() - .serve(stdio()) - .await - .inspect_err(|e| { - tracing::error!("serving error: {:?}", e); - })?; - - service.waiting().await?; - return Ok(()); - } - - if name == "memory" { - let service = MemoryServer::new().serve(stdio()).await.inspect_err(|e| { - tracing::error!("serving error: {:?}", e); - })?; - service.waiting().await?; - return Ok(()); - } - - // Handle old MCP-based servers - let router: Option> = match name { - "computercontroller" => Some(Box::new(RouterService(ComputerControllerRouter::new()))), - _ => None, - }; - - let server = Server::new(router.unwrap_or_else(|| panic!("Unknown server requested {}", name))); - let transport = ByteTransport::new(stdin(), stdout()); - - tracing::info!("Server initialized and ready to handle requests"); - Ok(server.run(transport).await?) -} diff --git a/crates/goose-server/src/commands/mod.rs b/crates/goose-server/src/commands/mod.rs index 9de800dea7..f17bc55db8 100644 --- a/crates/goose-server/src/commands/mod.rs +++ b/crates/goose-server/src/commands/mod.rs @@ -1,2 +1 @@ pub mod agent; -pub mod mcp; diff --git a/crates/goose-server/src/main.rs b/crates/goose-server/src/main.rs index ccd285687d..d8e96fddb3 100644 --- a/crates/goose-server/src/main.rs +++ b/crates/goose-server/src/main.rs @@ -36,7 +36,8 @@ async fn main() -> anyhow::Result<()> { commands::agent::run().await?; } Commands::Mcp { name } => { - commands::mcp::run(name).await?; + logging::setup_logging(Some(&format!("mcp-{name}")))?; + goose_mcp::mcp_server_runner::run_mcp_server(name).await?; } }