diff --git a/crates/goose-mcp/src/developer/mod.rs b/crates/goose-mcp/src/developer/mod.rs index a55372cb09..f85096a2da 100644 --- a/crates/goose-mcp/src/developer/mod.rs +++ b/crates/goose-mcp/src/developer/mod.rs @@ -15,8 +15,10 @@ use std::{ use tokio::process::Command; use url::Url; +use include_dir::{include_dir, Dir}; +use mcp_core::prompt::{Prompt, PromptArgument, PromptTemplate}; use mcp_core::{ - handler::{ResourceError, ToolError}, + handler::{PromptError, ResourceError, ToolError}, protocol::ServerCapabilities, resource::Resource, tool::Tool, @@ -27,20 +29,66 @@ use mcp_server::Router; use mcp_core::content::Content; use mcp_core::role::Role; +use self::shell::{ + expand_path, format_command_for_platform, get_shell_config, is_absolute_path, + normalize_line_endings, +}; use indoc::indoc; use std::process::Stdio; use std::sync::{Arc, Mutex}; use xcap::{Monitor, Window}; -use self::shell::{ - expand_path, format_command_for_platform, get_shell_config, is_absolute_path, - normalize_line_endings, -}; +// Embeds the prompts directory to the build +static PROMPTS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/src/developer/prompts"); + +/// Loads prompt files from the embedded PROMPTS_DIR and returns a HashMap of prompts. +/// Ensures that each prompt name is unique. +pub fn load_prompt_files() -> HashMap { + let mut prompts = HashMap::new(); + + for entry in PROMPTS_DIR.files() { + let prompt_str = String::from_utf8_lossy(entry.contents()).into_owned(); + + let template: PromptTemplate = match serde_json::from_str(&prompt_str) { + Ok(t) => t, + Err(e) => { + eprintln!( + "Failed to parse prompt template in {}: {}", + entry.path().display(), + e + ); + continue; // Skip invalid prompt file + } + }; + + let arguments = template + .arguments + .into_iter() + .map(|arg| PromptArgument { + name: arg.name, + description: arg.description, + required: arg.required, + }) + .collect(); + + let prompt = Prompt::new(&template.id, &template.template, arguments); + + if prompts.contains_key(&prompt.name) { + eprintln!("Duplicate prompt name '{}' found. Skipping.", prompt.name); + continue; // Skip duplicate prompt name + } + + prompts.insert(prompt.name.clone(), prompt); + } + + prompts +} pub struct DeveloperRouter { tools: Vec, - file_history: Arc>>>, + prompts: Arc>, instructions: String, + file_history: Arc>>>, } impl Default for DeveloperRouter { @@ -273,8 +321,9 @@ impl DeveloperRouter { list_windows_tool, screen_capture_tool, ], - file_history: Arc::new(Mutex::new(HashMap::new())), + prompts: Arc::new(load_prompt_files()), instructions, + file_history: Arc::new(Mutex::new(HashMap::new())), } } @@ -726,7 +775,10 @@ impl Router for DeveloperRouter { } fn capabilities(&self) -> ServerCapabilities { - CapabilitiesBuilder::new().with_tools(false).build() + CapabilitiesBuilder::new() + .with_tools(false) + .with_prompts(false) + .build() } fn list_tools(&self) -> Vec { @@ -762,14 +814,58 @@ impl Router for DeveloperRouter { ) -> Pin> + Send + 'static>> { Box::pin(async move { Ok("".to_string()) }) } + + fn list_prompts(&self) -> Option> { + if self.prompts.is_empty() { + None + } else { + Some(self.prompts.values().cloned().collect()) + } + } + + fn get_prompt( + &self, + prompt_name: &str, + ) -> Option> + Send + 'static>>> { + let prompt_name = prompt_name.trim().to_owned(); + + // Validate prompt name is not empty + if prompt_name.is_empty() { + return Some(Box::pin(async move { + Err(PromptError::InvalidParameters( + "Prompt name cannot be empty".to_string(), + )) + })); + } + + let prompts = Arc::clone(&self.prompts); + + Some(Box::pin(async move { + match prompts.get(&prompt_name) { + Some(prompt) => { + if prompt.description.trim().is_empty() { + Err(PromptError::InternalError(format!( + "Prompt '{prompt_name}' has an empty description" + ))) + } else { + Ok(prompt.description.clone()) + } + } + None => Err(PromptError::NotFound(format!( + "Prompt '{prompt_name}' not found" + ))), + } + })) + } } impl Clone for DeveloperRouter { fn clone(&self) -> Self { Self { tools: self.tools.clone(), - file_history: Arc::clone(&self.file_history), + prompts: Arc::clone(&self.prompts), instructions: self.instructions.clone(), + file_history: Arc::clone(&self.file_history), } } } diff --git a/crates/goose-mcp/src/developer/prompts/unit_test.json b/crates/goose-mcp/src/developer/prompts/unit_test.json new file mode 100644 index 0000000000..abef9414f4 --- /dev/null +++ b/crates/goose-mcp/src/developer/prompts/unit_test.json @@ -0,0 +1,16 @@ +{ + "id": "unit_test", + "template": "Generate or update unit tests for a given source code file.\n\nThe source code file is provided in {source_code}.\nPlease update the existing tests, ensure they are passing, and add any new tests as needed.\n\nThe test suite should:\n- Follow language-specific test naming conventions for {language}\n- Include all necessary imports and annotations\n- Thoroughly test the specified functionality\n- Ensure tests are passing before completion\n- Handle edge cases and error conditions\n- Use clear test names that reflect what is being tested", + "arguments": [ + { + "name": "source_code", + "description": "The source code file content to be tested", + "required": true + }, + { + "name": "language", + "description": "The programming language of the source code", + "required": true + } + ] + } \ No newline at end of file