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
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -4642,6 +4642,7 @@ dependencies = [
|
|||||||
"openssl-sys",
|
"openssl-sys",
|
||||||
"os_info",
|
"os_info",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
|
"regex",
|
||||||
"remote",
|
"remote",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"rmcp",
|
"rmcp",
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ url = "2.5"
|
|||||||
rand = { version = "0.8", features = ["std"] }
|
rand = { version = "0.8", features = ["std"] }
|
||||||
sha2 = "0.10"
|
sha2 = "0.10"
|
||||||
strum = "0.27.2"
|
strum = "0.27.2"
|
||||||
|
regex = "1"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
dotenv = "0.15"
|
dotenv = "0.15"
|
||||||
|
|||||||
@@ -2,10 +2,12 @@ use std::{future::Future, path::PathBuf, str::FromStr};
|
|||||||
|
|
||||||
use db::models::{
|
use db::models::{
|
||||||
project::Project,
|
project::Project,
|
||||||
|
tag::Tag,
|
||||||
task::{CreateTask, Task, TaskStatus, TaskWithAttemptStatus, UpdateTask},
|
task::{CreateTask, Task, TaskStatus, TaskWithAttemptStatus, UpdateTask},
|
||||||
task_attempt::{TaskAttempt, TaskAttemptContext},
|
task_attempt::{TaskAttempt, TaskAttemptContext},
|
||||||
};
|
};
|
||||||
use executors::{executors::BaseCodingAgent, profile::ExecutorProfileId};
|
use executors::{executors::BaseCodingAgent, profile::ExecutorProfileId};
|
||||||
|
use regex::Regex;
|
||||||
use rmcp::{
|
use rmcp::{
|
||||||
ErrorData, ServerHandler,
|
ErrorData, ServerHandler,
|
||||||
handler::server::tool::{Parameters, ToolRouter},
|
handler::server::tool::{Parameters, ToolRouter},
|
||||||
@@ -385,6 +387,58 @@ impl TaskServer {
|
|||||||
path.trim_start_matches('/')
|
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]
|
#[tool_router]
|
||||||
@@ -409,6 +463,12 @@ impl TaskServer {
|
|||||||
description,
|
description,
|
||||||
}): Parameters<CreateTaskRequest>,
|
}): Parameters<CreateTaskRequest>,
|
||||||
) -> Result<CallToolResult, ErrorData> {
|
) -> 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 url = self.url("/api/tasks");
|
||||||
let task: Task = match self
|
let task: Task = match self
|
||||||
.send_json(
|
.send_json(
|
||||||
@@ -417,7 +477,7 @@ impl TaskServer {
|
|||||||
.json(&CreateTask::from_title_description(
|
.json(&CreateTask::from_title_description(
|
||||||
project_id,
|
project_id,
|
||||||
title,
|
title,
|
||||||
description,
|
expanded_description,
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
@@ -604,9 +664,15 @@ impl TaskServer {
|
|||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Expand @tagname references in description
|
||||||
|
let expanded_description = match description {
|
||||||
|
Some(desc) => Some(self.expand_tags(&desc).await),
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
let payload = UpdateTask {
|
let payload = UpdateTask {
|
||||||
title,
|
title,
|
||||||
description,
|
description: expanded_description,
|
||||||
status,
|
status,
|
||||||
parent_task_attempt: None,
|
parent_task_attempt: None,
|
||||||
image_ids: None,
|
image_ids: None,
|
||||||
|
|||||||
Reference in New Issue
Block a user