diff --git a/Cargo.lock b/Cargo.lock index b556847a..d87f9972 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4642,6 +4642,7 @@ dependencies = [ "openssl-sys", "os_info", "rand 0.8.5", + "regex", "remote", "reqwest", "rmcp", diff --git a/crates/server/Cargo.toml b/crates/server/Cargo.toml index 8b06e81d..3705a3c0 100644 --- a/crates/server/Cargo.toml +++ b/crates/server/Cargo.toml @@ -47,6 +47,7 @@ url = "2.5" rand = { version = "0.8", features = ["std"] } sha2 = "0.10" strum = "0.27.2" +regex = "1" [build-dependencies] dotenv = "0.15" diff --git a/crates/server/src/mcp/task_server.rs b/crates/server/src/mcp/task_server.rs index 4d791346..61ba8fad 100644 --- a/crates/server/src/mcp/task_server.rs +++ b/crates/server/src/mcp/task_server.rs @@ -2,10 +2,12 @@ use std::{future::Future, path::PathBuf, str::FromStr}; use db::models::{ project::Project, + tag::Tag, task::{CreateTask, Task, TaskStatus, TaskWithAttemptStatus, UpdateTask}, task_attempt::{TaskAttempt, TaskAttemptContext}, }; use executors::{executors::BaseCodingAgent, profile::ExecutorProfileId}; +use regex::Regex; use rmcp::{ ErrorData, ServerHandler, handler::server::tool::{Parameters, ToolRouter}, @@ -385,6 +387,58 @@ impl TaskServer { path.trim_start_matches('/') ) } + + /// Expands @tagname references in text by replacing them with tag content. + /// Returns the original text if expansion fails (e.g., network error). + /// Unknown tags are left as-is (not expanded, not an error). + async fn expand_tags(&self, text: &str) -> String { + // Pattern matches @tagname where tagname is non-whitespace, non-@ characters + let tag_pattern = match Regex::new(r"@([^\s@]+)") { + Ok(re) => re, + Err(_) => return text.to_string(), + }; + + // Find all unique tag names referenced in the text + let tag_names: Vec = tag_pattern + .captures_iter(text) + .filter_map(|cap| cap.get(1).map(|m| m.as_str().to_string())) + .collect::>() + .into_iter() + .collect(); + + if tag_names.is_empty() { + return text.to_string(); + } + + // Fetch all tags from the API + let url = self.url("/api/tags"); + let tags: Vec = match self.client.get(&url).send().await { + Ok(resp) if resp.status().is_success() => { + match resp.json::>>().await { + Ok(envelope) if envelope.success => envelope.data.unwrap_or_default(), + _ => return text.to_string(), + } + } + _ => return text.to_string(), + }; + + // Build a map of tag_name -> content for quick lookup + let tag_map: std::collections::HashMap<&str, &str> = tags + .iter() + .map(|t| (t.tag_name.as_str(), t.content.as_str())) + .collect(); + + // Replace each @tagname with its content (if found) + let result = tag_pattern.replace_all(text, |caps: ®ex::Captures| { + let tag_name = caps.get(1).map(|m| m.as_str()).unwrap_or(""); + match tag_map.get(tag_name) { + Some(content) => (*content).to_string(), + None => caps.get(0).map(|m| m.as_str()).unwrap_or("").to_string(), + } + }); + + result.into_owned() + } } #[tool_router] @@ -409,6 +463,12 @@ impl TaskServer { description, }): Parameters, ) -> Result { + // Expand @tagname references in description + let expanded_description = match description { + Some(desc) => Some(self.expand_tags(&desc).await), + None => None, + }; + let url = self.url("/api/tasks"); let task: Task = match self .send_json( @@ -417,7 +477,7 @@ impl TaskServer { .json(&CreateTask::from_title_description( project_id, title, - description, + expanded_description, )), ) .await @@ -604,9 +664,15 @@ impl TaskServer { None }; + // Expand @tagname references in description + let expanded_description = match description { + Some(desc) => Some(self.expand_tags(&desc).await), + None => None, + }; + let payload = UpdateTask { title, - description, + description: expanded_description, status, parent_task_attempt: None, image_ids: None,