MCP server expand tag (vibe-kanban) (#1458)
* Implementation is complete. Let me provide a summary of the changes made:
## Summary
I've implemented tag expansion for the MCP server. Here are the changes:
### Files Modified
1. **`crates/server/Cargo.toml`** - Added `regex = "1"` dependency
2. **`crates/server/src/mcp/task_server.rs`** - Three changes:
- Added imports for `Tag` and `Regex`
- Added `expand_tags()` helper function that:
- Finds all `@tagname` patterns using regex `@([^\s@]+)`
- Fetches tags from `/api/tags` endpoint
- Replaces each `@tagname` with the tag's content
- Returns original text if tag not found or on error
- Updated `create_task` to expand tags in description before creating
- Updated `update_task` to expand tags in description before updating
- Updated tool descriptions to mention tag expansion support
### How It Works
When an AI agent calls `create_task` or `update_task` with a description containing `@tagname` (e.g., `"Please implement this feature @requirements"`), the MCP server will:
1. Parse the description for `@tagname` patterns
2. Fetch all available tags from the backend API
3. Replace each `@tagname` with the corresponding tag's `content`
4. Save the task with the expanded description
If a tag is not found, the `@tagname` is left as-is. If there's a network error, the original text is preserved to ensure task creation doesn't fail.
* Update crates/server/src/mcp/task_server.rs
Co-authored-by: Solomon <abcpro11051@disroot.org>
* Update crates/server/src/mcp/task_server.rs
Co-authored-by: Solomon <abcpro11051@disroot.org>
---------
Co-authored-by: Solomon <abcpro11051@disroot.org>
This commit is contained in:
committed by
GitHub
parent
21d175bccc
commit
e83e0ee169
@@ -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"
|
||||
|
||||
@@ -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<String> = tag_pattern
|
||||
.captures_iter(text)
|
||||
.filter_map(|cap| cap.get(1).map(|m| m.as_str().to_string()))
|
||||
.collect::<std::collections::HashSet<_>>()
|
||||
.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<Tag> = match self.client.get(&url).send().await {
|
||||
Ok(resp) if resp.status().is_success() => {
|
||||
match resp.json::<ApiResponseEnvelope<Vec<Tag>>>().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<CreateTaskRequest>,
|
||||
) -> Result<CallToolResult, ErrorData> {
|
||||
// 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,
|
||||
|
||||
Reference in New Issue
Block a user