feat: easy vibe_kanban MCP config (#26)
* inject project_id into prompt; remove create_project tool * path hack for vibe-kanban mcp button * update mcp name * update mcp configuration * merge stderr into stdout for mcp process * add -y to mcp server config * fmt * fmt * revert reversion of cli.js * rename mcp server to vibe-kanban * improve tool descriptions
This commit is contained in:
committed by
GitHub
parent
17ed214c62
commit
8037946500
1
.gitignore
vendored
1
.gitignore
vendored
@@ -81,3 +81,4 @@ backend/db.sqlite
|
||||
|
||||
dev_assets
|
||||
/frontend/.env.sentry-build-plugin
|
||||
.ssh
|
||||
|
||||
@@ -38,7 +38,12 @@ impl Executor for AmpExecutor {
|
||||
use tokio::{io::AsyncWriteExt, process::Command};
|
||||
|
||||
let prompt = format!(
|
||||
"Task title: {}\nTask description: {}",
|
||||
r#"project_id: {}
|
||||
|
||||
Task title: {}
|
||||
Task description: {}
|
||||
"#,
|
||||
task.project_id,
|
||||
task.title,
|
||||
task.description
|
||||
.as_deref()
|
||||
|
||||
@@ -35,8 +35,12 @@ impl Executor for ClaudeExecutor {
|
||||
.ok_or(ExecutorError::TaskNotFound)?;
|
||||
|
||||
let prompt = format!(
|
||||
"Task title: {}
|
||||
Task description: {}",
|
||||
r#"project_id: {}
|
||||
|
||||
Task title: {}
|
||||
Task description: {}
|
||||
"#,
|
||||
task.project_id,
|
||||
task.title,
|
||||
task.description
|
||||
.as_deref()
|
||||
|
||||
@@ -34,7 +34,12 @@ impl Executor for GeminiExecutor {
|
||||
.ok_or(ExecutorError::TaskNotFound)?;
|
||||
|
||||
let prompt = format!(
|
||||
"Task title: {}\nTask description: {}",
|
||||
r#"project_id: {}
|
||||
|
||||
Task title: {}
|
||||
Task description: {}
|
||||
"#,
|
||||
task.project_id,
|
||||
task.title,
|
||||
task.description
|
||||
.as_deref()
|
||||
|
||||
@@ -35,7 +35,12 @@ impl Executor for OpencodeExecutor {
|
||||
use tokio::process::Command;
|
||||
|
||||
let prompt = format!(
|
||||
"Task title: {}\nTask description: {}",
|
||||
r#"project_id: {}
|
||||
|
||||
Task title: {}
|
||||
Task description: {}
|
||||
"#,
|
||||
task.project_id,
|
||||
task.title,
|
||||
task.description
|
||||
.as_deref()
|
||||
|
||||
@@ -10,13 +10,13 @@ use sqlx::SqlitePool;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::models::{
|
||||
project::{CreateProject, Project},
|
||||
project::Project,
|
||||
task::{CreateTask, Task, TaskStatus},
|
||||
};
|
||||
|
||||
#[derive(Debug, Deserialize, schemars::JsonSchema)]
|
||||
pub struct CreateTaskRequest {
|
||||
#[schemars(description = "The ID of the project to create the task in")]
|
||||
#[schemars(description = "The ID of the project to create the task in. This is required!")]
|
||||
pub project_id: String,
|
||||
#[schemars(description = "The title of the task")]
|
||||
pub title: String,
|
||||
@@ -170,54 +170,6 @@ pub struct DeleteTaskResponse {
|
||||
pub deleted_task_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, schemars::JsonSchema)]
|
||||
pub struct CompleteTaskRequest {
|
||||
#[schemars(description = "The ID of the project containing the task")]
|
||||
pub project_id: String,
|
||||
#[schemars(description = "The title of the task to complete (will search for exact match)")]
|
||||
pub task_title: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, schemars::JsonSchema)]
|
||||
pub struct SetTaskStatusRequest {
|
||||
#[schemars(description = "The ID of the project containing the task")]
|
||||
pub project_id: String,
|
||||
#[schemars(description = "The title of the task to update")]
|
||||
pub task_title: String,
|
||||
#[schemars(
|
||||
description = "New status: 'todo', 'in-progress', 'in-review', 'done', 'cancelled'"
|
||||
)]
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, schemars::JsonSchema)]
|
||||
pub struct UpdateTaskTitleRequest {
|
||||
#[schemars(description = "The ID of the project containing the task")]
|
||||
pub project_id: String,
|
||||
#[schemars(description = "Current title of the task")]
|
||||
pub current_title: String,
|
||||
#[schemars(description = "New title for the task")]
|
||||
pub new_title: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, schemars::JsonSchema)]
|
||||
pub struct UpdateTaskDescriptionRequest {
|
||||
#[schemars(description = "The ID of the project containing the task")]
|
||||
pub project_id: String,
|
||||
#[schemars(description = "The title of the task to update")]
|
||||
pub task_title: String,
|
||||
#[schemars(description = "New description for the task")]
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, schemars::JsonSchema)]
|
||||
pub struct DeleteTaskByTitleRequest {
|
||||
#[schemars(description = "The ID of the project containing the task")]
|
||||
pub project_id: String,
|
||||
#[schemars(description = "The title of the task to delete")]
|
||||
pub task_title: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, schemars::JsonSchema)]
|
||||
pub struct SimpleTaskResponse {
|
||||
pub success: bool,
|
||||
@@ -241,28 +193,6 @@ pub struct GetTaskResponse {
|
||||
pub project_name: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, schemars::JsonSchema)]
|
||||
pub struct CreateProjectRequest {
|
||||
#[schemars(description = "Name of the project")]
|
||||
pub name: String,
|
||||
#[schemars(description = "Path to the git repository")]
|
||||
pub git_repo_path: String,
|
||||
#[schemars(description = "Whether to use existing repo (true) or create new (false)")]
|
||||
pub use_existing_repo: Option<bool>,
|
||||
#[schemars(description = "Optional setup script command")]
|
||||
pub setup_script: Option<String>,
|
||||
#[schemars(description = "Optional development script command")]
|
||||
pub dev_script: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, schemars::JsonSchema)]
|
||||
pub struct CreateProjectResponse {
|
||||
pub success: bool,
|
||||
pub message: String,
|
||||
pub project_id: Option<String>,
|
||||
pub project: Option<ProjectSummary>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TaskServer {
|
||||
pub pool: SqlitePool,
|
||||
@@ -277,7 +207,9 @@ impl TaskServer {
|
||||
|
||||
#[tool(tool_box)]
|
||||
impl TaskServer {
|
||||
#[tool(description = "Create a new task in a project")]
|
||||
#[tool(
|
||||
description = "Create a new task/ticket in a project. Always pass the `project_id` of the project you want to create the task in - it is required!"
|
||||
)]
|
||||
async fn create_task(
|
||||
&self,
|
||||
#[tool(aggr)] CreateTaskRequest {
|
||||
@@ -365,7 +297,7 @@ impl TaskServer {
|
||||
}
|
||||
}
|
||||
|
||||
#[tool(description = "List all available projects")]
|
||||
#[tool(description = "List all the available projects")]
|
||||
async fn list_projects(
|
||||
&self,
|
||||
#[tool(aggr)] _request: ListProjectsRequest,
|
||||
@@ -415,7 +347,9 @@ impl TaskServer {
|
||||
}
|
||||
}
|
||||
|
||||
#[tool(description = "List tasks in a project with optional filtering and execution status")]
|
||||
#[tool(
|
||||
description = "List all the task/tickets in a project with optional filtering and execution status. `project_id` is required!"
|
||||
)]
|
||||
async fn list_tasks(
|
||||
&self,
|
||||
#[tool(aggr)] ListTasksRequest {
|
||||
@@ -552,7 +486,9 @@ impl TaskServer {
|
||||
}
|
||||
}
|
||||
|
||||
#[tool(description = "Update an existing task's title, description, or status")]
|
||||
#[tool(
|
||||
description = "Update an existing task/ticket's title, description, or status. `project_id` and `task_id` are required! `title`, `description`, and `status` are optional."
|
||||
)]
|
||||
async fn update_task(
|
||||
&self,
|
||||
#[tool(aggr)] UpdateTaskRequest {
|
||||
@@ -685,405 +621,9 @@ impl TaskServer {
|
||||
}
|
||||
}
|
||||
|
||||
#[tool(description = "Mark a task as completed by its title")]
|
||||
async fn complete_task(
|
||||
&self,
|
||||
#[tool(aggr)] CompleteTaskRequest {
|
||||
project_id,
|
||||
task_title,
|
||||
}: CompleteTaskRequest,
|
||||
) -> Result<CallToolResult, RmcpError> {
|
||||
let project_uuid = match Uuid::parse_str(&project_id) {
|
||||
Ok(uuid) => uuid,
|
||||
Err(_) => {
|
||||
let error_response = serde_json::json!({
|
||||
"success": false,
|
||||
"error": "Invalid project ID format. Please use the project ID from list_projects.",
|
||||
"hint": "Try using 'list_projects' first to get the correct project ID"
|
||||
});
|
||||
return Ok(CallToolResult::error(vec![Content::text(
|
||||
serde_json::to_string_pretty(&error_response).unwrap(),
|
||||
)]));
|
||||
}
|
||||
};
|
||||
|
||||
match Task::find_task_by_title(&self.pool, project_uuid, &task_title).await {
|
||||
Ok(Some(task)) => {
|
||||
match Task::update_status(&self.pool, task.id, project_uuid, TaskStatus::Done).await
|
||||
{
|
||||
Ok(_) => {
|
||||
let response = SimpleTaskResponse {
|
||||
success: true,
|
||||
message: format!("Task '{}' marked as completed", task_title),
|
||||
task_title: task_title.clone(),
|
||||
new_status: Some("done".to_string()),
|
||||
};
|
||||
Ok(CallToolResult::success(vec![Content::text(
|
||||
serde_json::to_string_pretty(&response).unwrap(),
|
||||
)]))
|
||||
}
|
||||
Err(e) => {
|
||||
let error_response = serde_json::json!({
|
||||
"success": false,
|
||||
"error": "Failed to update task status",
|
||||
"details": e.to_string()
|
||||
});
|
||||
Ok(CallToolResult::error(vec![Content::text(
|
||||
serde_json::to_string_pretty(&error_response).unwrap(),
|
||||
)]))
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(None) => {
|
||||
let error_response = serde_json::json!({
|
||||
"success": false,
|
||||
"error": format!("Task with title '{}' not found in project", task_title),
|
||||
"hint": "Use 'list_tasks' to see available tasks in the project"
|
||||
});
|
||||
Ok(CallToolResult::error(vec![Content::text(
|
||||
serde_json::to_string_pretty(&error_response).unwrap(),
|
||||
)]))
|
||||
}
|
||||
Err(e) => {
|
||||
let error_response = serde_json::json!({
|
||||
"success": false,
|
||||
"error": "Database error while searching for task",
|
||||
"details": e.to_string()
|
||||
});
|
||||
Ok(CallToolResult::error(vec![Content::text(
|
||||
serde_json::to_string_pretty(&error_response).unwrap(),
|
||||
)]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tool(description = "Set a task's status by its title")]
|
||||
async fn set_task_status(
|
||||
&self,
|
||||
#[tool(aggr)] SetTaskStatusRequest {
|
||||
project_id,
|
||||
task_title,
|
||||
status,
|
||||
}: SetTaskStatusRequest,
|
||||
) -> Result<CallToolResult, RmcpError> {
|
||||
let project_uuid = match Uuid::parse_str(&project_id) {
|
||||
Ok(uuid) => uuid,
|
||||
Err(_) => {
|
||||
let error_response = serde_json::json!({
|
||||
"success": false,
|
||||
"error": "Invalid project ID format",
|
||||
"hint": "Use 'list_projects' to get the correct project ID"
|
||||
});
|
||||
return Ok(CallToolResult::error(vec![Content::text(
|
||||
serde_json::to_string_pretty(&error_response).unwrap(),
|
||||
)]));
|
||||
}
|
||||
};
|
||||
|
||||
let status_enum = match parse_task_status(&status) {
|
||||
Some(s) => s,
|
||||
None => {
|
||||
let error_response = serde_json::json!({
|
||||
"success": false,
|
||||
"error": "Invalid status value",
|
||||
"provided": status,
|
||||
"valid_options": ["todo", "in-progress", "in-review", "done", "cancelled"],
|
||||
"hint": "Use one of the valid status options"
|
||||
});
|
||||
return Ok(CallToolResult::error(vec![Content::text(
|
||||
serde_json::to_string_pretty(&error_response).unwrap(),
|
||||
)]));
|
||||
}
|
||||
};
|
||||
|
||||
match Task::find_task_by_title(&self.pool, project_uuid, &task_title).await {
|
||||
Ok(Some(task)) => {
|
||||
match Task::update_status(&self.pool, task.id, project_uuid, status_enum).await {
|
||||
Ok(_) => {
|
||||
let response = SimpleTaskResponse {
|
||||
success: true,
|
||||
message: format!(
|
||||
"Task '{}' status updated to '{}'",
|
||||
task_title, status
|
||||
),
|
||||
task_title: task_title.clone(),
|
||||
new_status: Some(status.clone()),
|
||||
};
|
||||
Ok(CallToolResult::success(vec![Content::text(
|
||||
serde_json::to_string_pretty(&response).unwrap(),
|
||||
)]))
|
||||
}
|
||||
Err(e) => {
|
||||
let error_response = serde_json::json!({
|
||||
"success": false,
|
||||
"error": "Failed to update task status",
|
||||
"details": e.to_string()
|
||||
});
|
||||
Ok(CallToolResult::error(vec![Content::text(
|
||||
serde_json::to_string_pretty(&error_response).unwrap(),
|
||||
)]))
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(None) => {
|
||||
let error_response = serde_json::json!({
|
||||
"success": false,
|
||||
"error": format!("Task '{}' not found in project", task_title),
|
||||
"hint": "Use 'list_tasks' to see available tasks"
|
||||
});
|
||||
Ok(CallToolResult::error(vec![Content::text(
|
||||
serde_json::to_string_pretty(&error_response).unwrap(),
|
||||
)]))
|
||||
}
|
||||
Err(e) => {
|
||||
let error_response = serde_json::json!({
|
||||
"success": false,
|
||||
"error": "Database error",
|
||||
"details": e.to_string()
|
||||
});
|
||||
Ok(CallToolResult::error(vec![Content::text(
|
||||
serde_json::to_string_pretty(&error_response).unwrap(),
|
||||
)]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tool(description = "Delete a task by its title")]
|
||||
async fn delete_task_by_title(
|
||||
&self,
|
||||
#[tool(aggr)] DeleteTaskByTitleRequest {
|
||||
project_id,
|
||||
task_title,
|
||||
}: DeleteTaskByTitleRequest,
|
||||
) -> Result<CallToolResult, RmcpError> {
|
||||
let project_uuid = match Uuid::parse_str(&project_id) {
|
||||
Ok(uuid) => uuid,
|
||||
Err(_) => {
|
||||
let error_response = serde_json::json!({
|
||||
"success": false,
|
||||
"error": "Invalid project ID format",
|
||||
"hint": "Use 'list_projects' to get the correct project ID"
|
||||
});
|
||||
return Ok(CallToolResult::error(vec![Content::text(
|
||||
serde_json::to_string_pretty(&error_response).unwrap(),
|
||||
)]));
|
||||
}
|
||||
};
|
||||
|
||||
match Task::find_task_by_title(&self.pool, project_uuid, &task_title).await {
|
||||
Ok(Some(task)) => match Task::delete(&self.pool, task.id, project_uuid).await {
|
||||
Ok(_) => {
|
||||
let response = SimpleTaskResponse {
|
||||
success: true,
|
||||
message: format!("Task '{}' deleted successfully", task_title),
|
||||
task_title: task_title.clone(),
|
||||
new_status: None,
|
||||
};
|
||||
Ok(CallToolResult::success(vec![Content::text(
|
||||
serde_json::to_string_pretty(&response).unwrap(),
|
||||
)]))
|
||||
}
|
||||
Err(e) => {
|
||||
let error_response = serde_json::json!({
|
||||
"success": false,
|
||||
"error": "Failed to delete task",
|
||||
"details": e.to_string()
|
||||
});
|
||||
Ok(CallToolResult::error(vec![Content::text(
|
||||
serde_json::to_string_pretty(&error_response).unwrap(),
|
||||
)]))
|
||||
}
|
||||
},
|
||||
Ok(None) => {
|
||||
let error_response = serde_json::json!({
|
||||
"success": false,
|
||||
"error": format!("Task '{}' not found in project", task_title),
|
||||
"hint": "Use 'list_tasks' to see available tasks"
|
||||
});
|
||||
Ok(CallToolResult::error(vec![Content::text(
|
||||
serde_json::to_string_pretty(&error_response).unwrap(),
|
||||
)]))
|
||||
}
|
||||
Err(e) => {
|
||||
let error_response = serde_json::json!({
|
||||
"success": false,
|
||||
"error": "Database error",
|
||||
"details": e.to_string()
|
||||
});
|
||||
Ok(CallToolResult::error(vec![Content::text(
|
||||
serde_json::to_string_pretty(&error_response).unwrap(),
|
||||
)]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tool(description = "Update a task's title by finding it with the current title")]
|
||||
async fn update_task_title(
|
||||
&self,
|
||||
#[tool(aggr)] UpdateTaskTitleRequest {
|
||||
project_id,
|
||||
current_title,
|
||||
new_title,
|
||||
}: UpdateTaskTitleRequest,
|
||||
) -> Result<CallToolResult, RmcpError> {
|
||||
let project_uuid = match Uuid::parse_str(&project_id) {
|
||||
Ok(uuid) => uuid,
|
||||
Err(_) => {
|
||||
let error_response = serde_json::json!({
|
||||
"success": false,
|
||||
"error": "Invalid project ID format",
|
||||
"hint": "Use 'list_projects' to get the correct project ID"
|
||||
});
|
||||
return Ok(CallToolResult::error(vec![Content::text(
|
||||
serde_json::to_string_pretty(&error_response).unwrap(),
|
||||
)]));
|
||||
}
|
||||
};
|
||||
|
||||
match Task::find_task_by_title(&self.pool, project_uuid, ¤t_title).await {
|
||||
Ok(Some(task)) => {
|
||||
// Update the task with new title, keeping other fields the same
|
||||
match Task::update(
|
||||
&self.pool,
|
||||
task.id,
|
||||
project_uuid,
|
||||
new_title.clone(),
|
||||
task.description,
|
||||
task.status,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(updated_task) => {
|
||||
let response = serde_json::json!({
|
||||
"success": true,
|
||||
"message": format!("Task title updated from '{}' to '{}'", current_title, new_title),
|
||||
"old_title": current_title,
|
||||
"new_title": new_title,
|
||||
"task_id": updated_task.id.to_string()
|
||||
});
|
||||
Ok(CallToolResult::success(vec![Content::text(
|
||||
serde_json::to_string_pretty(&response).unwrap(),
|
||||
)]))
|
||||
}
|
||||
Err(e) => {
|
||||
let error_response = serde_json::json!({
|
||||
"success": false,
|
||||
"error": "Failed to update task title",
|
||||
"details": e.to_string()
|
||||
});
|
||||
Ok(CallToolResult::error(vec![Content::text(
|
||||
serde_json::to_string_pretty(&error_response).unwrap(),
|
||||
)]))
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(None) => {
|
||||
let error_response = serde_json::json!({
|
||||
"success": false,
|
||||
"error": format!("Task with title '{}' not found in project", current_title),
|
||||
"hint": "Use 'list_tasks' to see available tasks with exact titles"
|
||||
});
|
||||
Ok(CallToolResult::error(vec![Content::text(
|
||||
serde_json::to_string_pretty(&error_response).unwrap(),
|
||||
)]))
|
||||
}
|
||||
Err(e) => {
|
||||
let error_response = serde_json::json!({
|
||||
"success": false,
|
||||
"error": "Database error while searching for task",
|
||||
"details": e.to_string()
|
||||
});
|
||||
Ok(CallToolResult::error(vec![Content::text(
|
||||
serde_json::to_string_pretty(&error_response).unwrap(),
|
||||
)]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tool(description = "Update a task's description by its title")]
|
||||
async fn update_task_description(
|
||||
&self,
|
||||
#[tool(aggr)] UpdateTaskDescriptionRequest {
|
||||
project_id,
|
||||
task_title,
|
||||
description,
|
||||
}: UpdateTaskDescriptionRequest,
|
||||
) -> Result<CallToolResult, RmcpError> {
|
||||
let project_uuid = match Uuid::parse_str(&project_id) {
|
||||
Ok(uuid) => uuid,
|
||||
Err(_) => {
|
||||
let error_response = serde_json::json!({
|
||||
"success": false,
|
||||
"error": "Invalid project ID format",
|
||||
"hint": "Use 'list_projects' to get the correct project ID"
|
||||
});
|
||||
return Ok(CallToolResult::error(vec![Content::text(
|
||||
serde_json::to_string_pretty(&error_response).unwrap(),
|
||||
)]));
|
||||
}
|
||||
};
|
||||
|
||||
match Task::find_task_by_title(&self.pool, project_uuid, &task_title).await {
|
||||
Ok(Some(task)) => {
|
||||
// Update the task with new description, keeping other fields the same
|
||||
match Task::update(
|
||||
&self.pool,
|
||||
task.id,
|
||||
project_uuid,
|
||||
task.title.clone(),
|
||||
Some(description.clone()),
|
||||
task.status,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(updated_task) => {
|
||||
let response = serde_json::json!({
|
||||
"success": true,
|
||||
"message": format!("Description updated for task '{}'", task_title),
|
||||
"task_title": task_title,
|
||||
"new_description": description,
|
||||
"task_id": updated_task.id.to_string()
|
||||
});
|
||||
Ok(CallToolResult::success(vec![Content::text(
|
||||
serde_json::to_string_pretty(&response).unwrap(),
|
||||
)]))
|
||||
}
|
||||
Err(e) => {
|
||||
let error_response = serde_json::json!({
|
||||
"success": false,
|
||||
"error": "Failed to update task description",
|
||||
"details": e.to_string()
|
||||
});
|
||||
Ok(CallToolResult::error(vec![Content::text(
|
||||
serde_json::to_string_pretty(&error_response).unwrap(),
|
||||
)]))
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(None) => {
|
||||
let error_response = serde_json::json!({
|
||||
"success": false,
|
||||
"error": format!("Task with title '{}' not found in project", task_title),
|
||||
"hint": "Use 'list_tasks' to see available tasks with exact titles"
|
||||
});
|
||||
Ok(CallToolResult::error(vec![Content::text(
|
||||
serde_json::to_string_pretty(&error_response).unwrap(),
|
||||
)]))
|
||||
}
|
||||
Err(e) => {
|
||||
let error_response = serde_json::json!({
|
||||
"success": false,
|
||||
"error": "Database error while searching for task",
|
||||
"details": e.to_string()
|
||||
});
|
||||
Ok(CallToolResult::error(vec![Content::text(
|
||||
serde_json::to_string_pretty(&error_response).unwrap(),
|
||||
)]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tool(description = "Delete a task from a project")]
|
||||
#[tool(
|
||||
description = "Delete a task/ticket from a project. `project_id` and `task_id` are required!"
|
||||
)]
|
||||
async fn delete_task(
|
||||
&self,
|
||||
#[tool(aggr)] DeleteTaskRequest {
|
||||
@@ -1175,7 +715,9 @@ impl TaskServer {
|
||||
}
|
||||
}
|
||||
|
||||
#[tool(description = "Get detailed information about a specific task")]
|
||||
#[tool(
|
||||
description = "Get detailed information about a specific task/ticket. `project_id` and `task_id` are required!"
|
||||
)]
|
||||
async fn get_task(
|
||||
&self,
|
||||
#[tool(aggr)] GetTaskRequest {
|
||||
@@ -1258,66 +800,6 @@ impl TaskServer {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tool(description = "Create a new project")]
|
||||
async fn create_project(
|
||||
&self,
|
||||
#[tool(aggr)] CreateProjectRequest {
|
||||
name,
|
||||
git_repo_path,
|
||||
use_existing_repo,
|
||||
setup_script,
|
||||
dev_script,
|
||||
}: CreateProjectRequest,
|
||||
) -> Result<CallToolResult, RmcpError> {
|
||||
let project_id = Uuid::new_v4();
|
||||
|
||||
let create_project_data = CreateProject {
|
||||
name: name.clone(),
|
||||
git_repo_path,
|
||||
use_existing_repo: use_existing_repo.unwrap_or(true),
|
||||
setup_script,
|
||||
dev_script,
|
||||
};
|
||||
|
||||
match Project::create(&self.pool, &create_project_data, project_id).await {
|
||||
Ok(project) => {
|
||||
let current_branch = project.get_current_branch().ok();
|
||||
let project_summary = ProjectSummary {
|
||||
id: project.id.to_string(),
|
||||
name: project.name,
|
||||
git_repo_path: project.git_repo_path,
|
||||
setup_script: project.setup_script,
|
||||
dev_script: project.dev_script,
|
||||
current_branch,
|
||||
created_at: project.created_at.to_rfc3339(),
|
||||
updated_at: project.updated_at.to_rfc3339(),
|
||||
};
|
||||
|
||||
let response = CreateProjectResponse {
|
||||
success: true,
|
||||
message: "Project created successfully".to_string(),
|
||||
project_id: Some(project.id.to_string()),
|
||||
project: Some(project_summary),
|
||||
};
|
||||
|
||||
Ok(CallToolResult::success(vec![Content::text(
|
||||
serde_json::to_string_pretty(&response).unwrap(),
|
||||
)]))
|
||||
}
|
||||
Err(e) => {
|
||||
let error_response = serde_json::json!({
|
||||
"success": false,
|
||||
"error": "Failed to create project",
|
||||
"details": e.to_string(),
|
||||
"project_name": name
|
||||
});
|
||||
Ok(CallToolResult::error(vec![Content::text(
|
||||
serde_json::to_string_pretty(&error_response).unwrap(),
|
||||
)]))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tool(tool_box)]
|
||||
@@ -1329,10 +811,10 @@ impl ServerHandler for TaskServer {
|
||||
.enable_tools()
|
||||
.build(),
|
||||
server_info: Implementation {
|
||||
name: "task-manager".to_string(),
|
||||
name: "vibe-kanban".to_string(),
|
||||
version: "1.0.0".to_string(),
|
||||
},
|
||||
instructions: Some("A task and project management server. AGENT-FRIENDLY TOOLS (use task titles): 'complete_task', 'set_task_status', 'update_task_title', 'update_task_description', 'delete_task_by_title'. STANDARD TOOLS: 'list_projects', 'create_project', 'list_tasks', 'create_task', 'get_task'. ADVANCED TOOLS (use UUIDs): 'update_task', 'delete_task'. Always use exact task titles from list_tasks results.".to_string()),
|
||||
instructions: Some("A task and project management server. If you need to create or update tickets or tasks then use these tools. Most of them absolutely require that you pass the `project_id` of the project that you are currently working on. This should be provided to you. TOOLS: 'list_projects', 'list_tasks', 'create_task', 'get_task', 'update_task', 'delete_task'. Make sure to pass `project_id` or `task_id` where required. You can use list tools to get the available ids.".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -312,22 +312,4 @@ impl Task {
|
||||
.await?;
|
||||
Ok(result.is_some())
|
||||
}
|
||||
|
||||
pub async fn find_task_by_title(
|
||||
pool: &SqlitePool,
|
||||
project_id: Uuid,
|
||||
title: &str,
|
||||
) -> Result<Option<Self>, sqlx::Error> {
|
||||
sqlx::query_as!(
|
||||
Task,
|
||||
r#"SELECT id as "id!: Uuid", project_id as "project_id!: Uuid", title, description, status as "status!: TaskStatus", created_at as "created_at!: DateTime<Utc>", updated_at as "updated_at!: DateTime<Utc>"
|
||||
FROM tasks
|
||||
WHERE project_id = $1 AND title = $2
|
||||
LIMIT 1"#,
|
||||
project_id,
|
||||
title
|
||||
)
|
||||
.fetch_optional(pool)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,6 +134,49 @@ export function McpServers() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfigureVibeKanban = async () => {
|
||||
if (!selectedMcpExecutor) return;
|
||||
|
||||
try {
|
||||
// Parse existing configuration
|
||||
const existingConfig = mcpServers.trim() ? JSON.parse(mcpServers) : {};
|
||||
|
||||
// Always use production MCP installation instructions
|
||||
const vibeKanbanConfig = {
|
||||
command: 'npx',
|
||||
args: ['-y', 'vibe-kanban', '--mcp'],
|
||||
};
|
||||
|
||||
// Add vibe_kanban to the existing configuration
|
||||
let updatedConfig;
|
||||
if (selectedMcpExecutor === 'amp') {
|
||||
updatedConfig = {
|
||||
...existingConfig,
|
||||
'amp.mcpServers': {
|
||||
...(existingConfig['amp.mcpServers'] || {}),
|
||||
vibe_kanban: vibeKanbanConfig,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
updatedConfig = {
|
||||
...existingConfig,
|
||||
mcpServers: {
|
||||
...(existingConfig.mcpServers || {}),
|
||||
vibe_kanban: vibeKanbanConfig,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Update the textarea with the new configuration
|
||||
const configJson = JSON.stringify(updatedConfig, null, 2);
|
||||
setMcpServers(configJson);
|
||||
setMcpError(null);
|
||||
} catch (err) {
|
||||
setMcpError('Failed to configure vibe-kanban MCP server');
|
||||
console.error('Error configuring vibe-kanban:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleApplyMcpServers = async () => {
|
||||
if (!selectedMcpExecutor) return;
|
||||
|
||||
@@ -329,6 +372,19 @@ export function McpServers() {
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="pt-4">
|
||||
<Button
|
||||
onClick={handleConfigureVibeKanban}
|
||||
disabled={mcpApplying || mcpLoading || !selectedMcpExecutor}
|
||||
className="w-64"
|
||||
>
|
||||
Add Vibe-Kanban MCP
|
||||
</Button>
|
||||
<p className="text-sm text-muted-foreground mt-2">
|
||||
Automatically adds the Vibe-Kanban MCP server.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
@@ -92,16 +92,18 @@ function extractAndRun(baseName, launch) {
|
||||
if (platform !== "win32") {
|
||||
try {
|
||||
fs.chmodSync(binPath, 0o755);
|
||||
} catch {}
|
||||
} catch { }
|
||||
}
|
||||
return launch(binPath);
|
||||
}
|
||||
|
||||
if (isMcpMode) {
|
||||
extractAndRun("vibe-kanban-mcp", (bin) => {
|
||||
const proc = spawn(bin, [], { stdio: ["pipe", "pipe", "inherit"] });
|
||||
const proc = spawn(bin, [], { stdio: ["pipe", "pipe", "pipe"] });
|
||||
process.stdin.pipe(proc.stdin);
|
||||
proc.stdout.pipe(process.stdout);
|
||||
proc.stderr.pipe(process.stdout);
|
||||
|
||||
proc.on("exit", (c) => process.exit(c || 0));
|
||||
proc.on("error", (e) => {
|
||||
console.error("❌ MCP server error:", e.message);
|
||||
|
||||
Reference in New Issue
Block a user