From 803794650073f5f542c87fc928d62c5421b79692 Mon Sep 17 00:00:00 2001 From: Gabriel Gordon-Hall Date: Mon, 7 Jul 2025 10:39:12 +0100 Subject: [PATCH] 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 --- .gitignore | 1 + backend/src/executors/amp.rs | 7 +- backend/src/executors/claude.rs | 8 +- backend/src/executors/gemini.rs | 7 +- backend/src/executors/opencode.rs | 7 +- backend/src/mcp/task_server.rs | 558 ++---------------------------- backend/src/models/task.rs | 18 - frontend/src/pages/McpServers.tsx | 56 +++ npx-cli/bin/cli.js | 6 +- 9 files changed, 105 insertions(+), 563 deletions(-) diff --git a/.gitignore b/.gitignore index 32be2889..57a64ef1 100644 --- a/.gitignore +++ b/.gitignore @@ -81,3 +81,4 @@ backend/db.sqlite dev_assets /frontend/.env.sentry-build-plugin +.ssh diff --git a/backend/src/executors/amp.rs b/backend/src/executors/amp.rs index 8c9b1c60..7a07a649 100644 --- a/backend/src/executors/amp.rs +++ b/backend/src/executors/amp.rs @@ -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() diff --git a/backend/src/executors/claude.rs b/backend/src/executors/claude.rs index 6b482af6..17ab4bab 100644 --- a/backend/src/executors/claude.rs +++ b/backend/src/executors/claude.rs @@ -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() diff --git a/backend/src/executors/gemini.rs b/backend/src/executors/gemini.rs index ef6d446e..2e35824c 100644 --- a/backend/src/executors/gemini.rs +++ b/backend/src/executors/gemini.rs @@ -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() diff --git a/backend/src/executors/opencode.rs b/backend/src/executors/opencode.rs index c510a7c1..bea687c8 100644 --- a/backend/src/executors/opencode.rs +++ b/backend/src/executors/opencode.rs @@ -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() diff --git a/backend/src/mcp/task_server.rs b/backend/src/mcp/task_server.rs index a99a0f25..875d8623 100644 --- a/backend/src/mcp/task_server.rs +++ b/backend/src/mcp/task_server.rs @@ -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, } -#[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, } -#[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, - #[schemars(description = "Optional setup script command")] - pub setup_script: Option, - #[schemars(description = "Optional development script command")] - pub dev_script: Option, -} - -#[derive(Debug, Serialize, schemars::JsonSchema)] -pub struct CreateProjectResponse { - pub success: bool, - pub message: String, - pub project_id: Option, - pub project: Option, -} - #[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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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()), } } } diff --git a/backend/src/models/task.rs b/backend/src/models/task.rs index aa56b149..22024dbf 100644 --- a/backend/src/models/task.rs +++ b/backend/src/models/task.rs @@ -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, 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", updated_at as "updated_at!: DateTime" - FROM tasks - WHERE project_id = $1 AND title = $2 - LIMIT 1"#, - project_id, - title - ) - .fetch_optional(pool) - .await - } } diff --git a/frontend/src/pages/McpServers.tsx b/frontend/src/pages/McpServers.tsx index 21918902..3b46d1cf 100644 --- a/frontend/src/pages/McpServers.tsx +++ b/frontend/src/pages/McpServers.tsx @@ -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() { )} + +
+ +

+ Automatically adds the Vibe-Kanban MCP server. +

+
)} diff --git a/npx-cli/bin/cli.js b/npx-cli/bin/cli.js index 2e5f9509..87c0dcdf 100755 --- a/npx-cli/bin/cli.js +++ b/npx-cli/bin/cli.js @@ -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);