feat: ticket ingestion MCP server (#1)

* basic ticket uploading

* take project_id in request params instead of env

* add an endpoint to list all available projects

* add mcp server bin to npx

* add missing scripts to package and publish to npm

* fix rmcp version

* Use utils::asset_dir

* Don't run migrations or create DB from MCP

* a fix for the first dev run when no frontend/dist/index.html exists

* Add more MCP endpoints (#8)

* add new endpoints for project and task management

* add simpler more focused endpoints to improve agent understanding on this MCP

* improve test script

* combine npm binaries and allow passing --mcp as an arg

* cargo fmt

* fixes after rebase

* clippy fixes

* Script tweaks

---------

Co-authored-by: couscous <couscous@runner.com>
Co-authored-by: anastasiya1155 <anastasiya1155@gmail.com>
Co-authored-by: Louis Knight-Webb <louis@bloop.ai>
Co-authored-by: Anastasiia Solop <35258279+anastasiya1155@users.noreply.github.com>
This commit is contained in:
Gabriel Gordon-Hall
2025-06-27 18:14:25 +01:00
committed by GitHub
parent 58f621c816
commit 0514d437a2
16 changed files with 2030 additions and 56 deletions

2
.gitignore vendored
View File

@@ -71,6 +71,6 @@ coverage/
frontend/dist frontend/dist
backend/bindings backend/bindings
build_codesign_release.sh build-npm-package-codesign.sh
npx-cli/dist npx-cli/dist

View File

@@ -0,0 +1,56 @@
{
"db_name": "SQLite",
"query": "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>\"\n FROM tasks \n WHERE project_id = $1 AND title = $2\n LIMIT 1",
"describe": {
"columns": [
{
"name": "id!: Uuid",
"ordinal": 0,
"type_info": "Blob"
},
{
"name": "project_id!: Uuid",
"ordinal": 1,
"type_info": "Blob"
},
{
"name": "title",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "description",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "status!: TaskStatus",
"ordinal": 4,
"type_info": "Text"
},
{
"name": "created_at!: DateTime<Utc>",
"ordinal": 5,
"type_info": "Text"
},
{
"name": "updated_at!: DateTime<Utc>",
"ordinal": 6,
"type_info": "Text"
}
],
"parameters": {
"Right": 2
},
"nullable": [
true,
false,
false,
true,
false,
false,
false
]
},
"hash": "7193dead2b112b137880482fe8e8c822c67ef6692e0456683331a438a4aa002f"
}

View File

@@ -3,6 +3,7 @@ name = "vibe-kanban"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"
default-run = "vibe-kanban" default-run = "vibe-kanban"
build = "build.rs"
[lib] [lib]
name = "vibe_kanban" name = "vibe_kanban"
@@ -35,6 +36,8 @@ open = "5.3.2"
ignore = "0.4" ignore = "0.4"
command-group = { version = "5.0", features = ["with-tokio"] } command-group = { version = "5.0", features = ["with-tokio"] }
openssl-sys = { workspace = true } openssl-sys = { workspace = true }
rmcp = { version = "0.1.5", features = ["server", "transport-io"] }
schemars = "0.8"
[build-dependencies] [build-dependencies]
ts-rs = { version = "9.0", features = ["uuid-impl", "chrono-impl"] } ts-rs = { version = "9.0", features = ["uuid-impl", "chrono-impl"] }

View File

@@ -1,4 +1,17 @@
use std::{fs, path::Path};
fn main() { fn main() {
// Tell cargo to rerun build script if models change // Create frontend/dist directory if it doesn't exist
println!("cargo:rerun-if-changed=src/models/"); let dist_path = Path::new("../frontend/dist");
if !dist_path.exists() {
println!("cargo:warning=Creating dummy frontend/dist directory for compilation");
fs::create_dir_all(dist_path).unwrap();
// Create a dummy index.html
let dummy_html = r#"<!DOCTYPE html>
<html><head><title>Build frontend first</title></head>
<body><h1>Please build the frontend</h1></body></html>"#;
fs::write(dist_path.join("index.html"), dummy_html).unwrap();
}
} }

View File

@@ -0,0 +1,34 @@
use std::str::FromStr;
use rmcp::{transport::stdio, ServiceExt};
use sqlx::{sqlite::SqliteConnectOptions, SqlitePool};
use vibe_kanban::{mcp::task_server::TaskServer, utils::asset_dir};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
tracing_subscriber::fmt()
.with_env_filter("debug")
.with_writer(std::io::stderr)
.init();
tracing::debug!("[MCP] Starting MCP task server...");
// Database connection
let database_url = format!(
"sqlite://{}",
asset_dir().join("db.sqlite").to_string_lossy()
);
let options = SqliteConnectOptions::from_str(&database_url)?.create_if_missing(false);
let pool = SqlitePool::connect_with(options).await?;
let service = TaskServer::new(pool)
.serve(stdio())
.await
.inspect_err(|e| {
tracing::error!("serving error: {:?}", e);
})?;
service.waiting().await?;
Ok(())
}

View File

@@ -2,6 +2,7 @@ pub mod app_state;
pub mod execution_monitor; pub mod execution_monitor;
pub mod executor; pub mod executor;
pub mod executors; pub mod executors;
pub mod mcp;
pub mod models; pub mod models;
pub mod routes; pub mod routes;
pub mod utils; pub mod utils;

View File

@@ -17,6 +17,7 @@ mod app_state;
mod execution_monitor; mod execution_monitor;
mod executor; mod executor;
mod executors; mod executors;
mod mcp;
mod models; mod models;
mod routes; mod routes;
mod utils; mod utils;

1
backend/src/mcp/mod.rs Normal file
View File

@@ -0,0 +1 @@
pub mod task_server;

File diff suppressed because it is too large Load Diff

View File

@@ -244,4 +244,22 @@ impl Task {
.await?; .await?;
Ok(result.is_some()) 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
}
} }

33
build-npm-package.sh Executable file
View File

@@ -0,0 +1,33 @@
#!/bin/bash
set -e # Exit on any error
echo "🧹 Cleaning previous builds..."
rm -rf npx-cli/dist
mkdir -p npx-cli/dist/macos-arm64
echo "🔨 Building frontend..."
npm run frontend:build
echo "🔨 Building Rust binaries..."
cargo build --release --manifest-path backend/Cargo.toml
cargo build --release --bin mcp_task_server --manifest-path backend/Cargo.toml
echo "📦 Creating distribution package..."
# Copy the main binary
cp target/release/vibe-kanban vibe-kanban
cp target/release/mcp_task_server vibe-kanban-mcp
zip vibe-kanban.zip vibe-kanban
zip vibe-kanban-mcp.zip vibe-kanban-mcp
rm vibe-kanban vibe-kanban-mcp
mv vibe-kanban.zip npx-cli/dist/macos-arm64/vibe-kanban.zip
mv vibe-kanban-mcp.zip npx-cli/dist/macos-arm64/vibe-kanban-mcp.zip
echo "✅ NPM package ready!"
echo "📁 Files created:"
echo " - npx-cli/dist/macos-arm64/vibe-kanban.zip"
echo " - npx-cli/dist/macos-arm64/vibe-kanban-mcp.zip"

374
mcp_test.js Normal file
View File

@@ -0,0 +1,374 @@
const { spawn } = require('child_process');
console.error('🔄 Starting MCP server for comprehensive endpoint testing...');
// Test configuration
let currentStepIndex = 0;
let messageId = 1;
let testData = {
projectId: null,
taskId: null,
createdProjectId: null,
taskTitle: "Test Task from MCP Script",
updatedTaskTitle: "Updated Test Task Title",
secondTaskTitle: "Second Test Task",
renamedTaskTitle: "Renamed Second Task",
};
const testSequence = [
'initialize',
'initialized_notification',
'list_tools',
'list_projects',
'create_project',
'list_tasks', // empty
'create_task',
'get_task',
'list_tasks', // with task
'set_task_status',
'list_tasks', // filtered
'complete_task',
'list_tasks', // completed
'create_task', // second task
'update_task', // legacy
'update_task_title',
'update_task_description',
'list_tasks', // after updates
'delete_task_by_title',
'list_tasks', // final
'summary'
];
const stepHandlers = {
initialize: {
description: 'Initialize MCP connection',
action: () => {
console.log('📤 Sending initialize request...');
mcpProcess.stdin.write(`{"jsonrpc": "2.0", "id": ${messageId++}, "method": "initialize", "params": {"protocolVersion": "2024-11-05", "capabilities": {}, "clientInfo": {"name": "test", "version": "1.0.0"}}}\n`);
},
responseHandler: () => {
executeNextStep();
}
},
initialized_notification: {
description: 'Send initialized notification',
action: () => {
console.log('📤 Sending initialized notification...');
mcpProcess.stdin.write('{"jsonrpc": "2.0", "method": "notifications/initialized"}\n');
// Notifications don't have responses, auto-advance
setTimeout(() => executeNextStep(), 200);
},
responseHandler: null
},
list_tools: {
description: 'List available MCP tools',
action: () => {
console.log('📤 Sending tools/list...');
mcpProcess.stdin.write(`{"jsonrpc": "2.0", "id": ${messageId++}, "method": "tools/list", "params": {}}\n`);
},
responseHandler: () => {
executeNextStep();
}
},
list_projects: {
description: 'List all projects',
action: () => {
console.log('📤 Sending list_projects...');
mcpProcess.stdin.write(`{"jsonrpc": "2.0", "id": ${messageId++}, "method": "tools/call", "params": {"name": "list_projects", "arguments": {}}}\n`);
},
responseHandler: (response) => {
try {
const parsedResponse = JSON.parse(response);
if (parsedResponse.result?.content) {
const projectsResponse = JSON.parse(parsedResponse.result.content[0].text);
if (projectsResponse.success && projectsResponse.projects.length > 0) {
testData.projectId = projectsResponse.projects[0].id;
console.log(`💾 Found existing project: ${testData.projectId}`);
}
}
} catch (e) {
console.error('⚠️ Could not parse projects response');
}
executeNextStep();
}
},
create_project: {
description: 'Create a new test project',
action: () => {
console.log('📤 Sending create_project...');
mcpProcess.stdin.write(`{"jsonrpc": "2.0", "id": ${messageId++}, "method": "tools/call", "params": {"name": "create_project", "arguments": {"name": "Test Project from MCP", "git_repo_path": "/tmp/test-project", "use_existing_repo": false, "setup_script": "echo \\"Setup complete\\"", "dev_script": "echo \\"Dev server started\\""}}}\n`);
},
responseHandler: (response) => {
try {
const parsedResponse = JSON.parse(response);
if (parsedResponse.result?.content) {
const createProjectResponse = JSON.parse(parsedResponse.result.content[0].text);
if (createProjectResponse.success && createProjectResponse.project_id) {
testData.createdProjectId = createProjectResponse.project_id;
console.log(`💾 Created project: ${testData.createdProjectId}`);
}
}
} catch (e) {
console.error('⚠️ Could not parse create project response');
}
executeNextStep();
}
},
list_tasks: {
description: 'List tasks in project',
action: () => {
const projectToUse = testData.createdProjectId || testData.projectId;
const context = getListTasksContext();
console.log(`📤 Sending list_tasks (${context})...`);
let args = { project_id: projectToUse };
// Add context-specific filters
if (context === 'filtered') {
args.status = 'in-progress';
} else if (context === 'completed') {
args.status = 'done';
} else if (context === 'empty') {
args.include_execution_status = true;
}
mcpProcess.stdin.write(`{"jsonrpc": "2.0", "id": ${messageId++}, "method": "tools/call", "params": {"name": "list_tasks", "arguments": ${JSON.stringify(args)}}}\n`);
},
responseHandler: () => {
executeNextStep();
}
},
create_task: {
description: 'Create a new task',
action: () => {
const projectToUse = testData.createdProjectId || testData.projectId;
const isSecondTask = getCreateTaskContext() === 'second';
const title = isSecondTask ? testData.secondTaskTitle : testData.taskTitle;
const description = isSecondTask ?
"This is a second task for testing updates" :
"This task was created during endpoint testing";
console.log(`📤 Sending create_task (${isSecondTask ? 'second task' : 'first task'})...`);
mcpProcess.stdin.write(`{"jsonrpc": "2.0", "id": ${messageId++}, "method": "tools/call", "params": {"name": "create_task", "arguments": {"project_id": "${projectToUse}", "title": "${title}", "description": "${description}"}}}\n`);
},
responseHandler: (response) => {
try {
const parsedResponse = JSON.parse(response);
if (parsedResponse.result?.content) {
const createTaskResponse = JSON.parse(parsedResponse.result.content[0].text);
if (createTaskResponse.success && createTaskResponse.task_id) {
testData.taskId = createTaskResponse.task_id;
console.log(`💾 Created task: ${testData.taskId}`);
}
}
} catch (e) {
console.error('⚠️ Could not parse create task response');
}
executeNextStep();
}
},
get_task: {
description: 'Get task details by ID',
action: () => {
const projectToUse = testData.createdProjectId || testData.projectId;
console.log('📤 Sending get_task...');
mcpProcess.stdin.write(`{"jsonrpc": "2.0", "id": ${messageId++}, "method": "tools/call", "params": {"name": "get_task", "arguments": {"project_id": "${projectToUse}", "task_id": "${testData.taskId}"}}}\n`);
},
responseHandler: () => {
executeNextStep();
}
},
set_task_status: {
description: 'Set task status (agent-friendly)',
action: () => {
const projectToUse = testData.createdProjectId || testData.projectId;
console.log('📤 Sending set_task_status (agent-friendly)...');
mcpProcess.stdin.write(`{"jsonrpc": "2.0", "id": ${messageId++}, "method": "tools/call", "params": {"name": "set_task_status", "arguments": {"project_id": "${projectToUse}", "task_title": "${testData.taskTitle}", "status": "in-progress"}}}\n`);
},
responseHandler: () => {
executeNextStep();
}
},
complete_task: {
description: 'Complete task (agent-friendly)',
action: () => {
const projectToUse = testData.createdProjectId || testData.projectId;
console.log('📤 Sending complete_task (agent-friendly)...');
mcpProcess.stdin.write(`{"jsonrpc": "2.0", "id": ${messageId++}, "method": "tools/call", "params": {"name": "complete_task", "arguments": {"project_id": "${projectToUse}", "task_title": "${testData.taskTitle}"}}}\n`);
},
responseHandler: () => {
executeNextStep();
}
},
update_task: {
description: 'Update task (legacy UUID method)',
action: () => {
const projectToUse = testData.createdProjectId || testData.projectId;
console.log('📤 Sending update_task (legacy method)...');
mcpProcess.stdin.write(`{"jsonrpc": "2.0", "id": ${messageId++}, "method": "tools/call", "params": {"name": "update_task", "arguments": {"project_id": "${projectToUse}", "task_id": "${testData.taskId}", "title": "${testData.updatedTaskTitle}", "description": "Updated description via legacy method", "status": "in-review"}}}\n`);
},
responseHandler: () => {
executeNextStep();
}
},
update_task_title: {
description: 'Update task title (agent-friendly)',
action: () => {
const projectToUse = testData.createdProjectId || testData.projectId;
console.log('📤 Sending update_task_title (agent-friendly)...');
mcpProcess.stdin.write(`{"jsonrpc": "2.0", "id": ${messageId++}, "method": "tools/call", "params": {"name": "update_task_title", "arguments": {"project_id": "${projectToUse}", "current_title": "${testData.secondTaskTitle}", "new_title": "${testData.renamedTaskTitle}"}}}\n`);
},
responseHandler: () => {
executeNextStep();
}
},
update_task_description: {
description: 'Update task description (agent-friendly)',
action: () => {
const projectToUse = testData.createdProjectId || testData.projectId;
console.log('📤 Sending update_task_description (agent-friendly)...');
mcpProcess.stdin.write(`{"jsonrpc": "2.0", "id": ${messageId++}, "method": "tools/call", "params": {"name": "update_task_description", "arguments": {"project_id": "${projectToUse}", "task_title": "${testData.renamedTaskTitle}", "description": "This description was updated using the agent-friendly endpoint"}}}\n`);
},
responseHandler: () => {
executeNextStep();
}
},
delete_task_by_title: {
description: 'Delete task by title (agent-friendly)',
action: () => {
const projectToUse = testData.createdProjectId || testData.projectId;
console.log('📤 Sending delete_task_by_title (agent-friendly)...');
mcpProcess.stdin.write(`{"jsonrpc": "2.0", "id": ${messageId++}, "method": "tools/call", "params": {"name": "delete_task_by_title", "arguments": {"project_id": "${projectToUse}", "task_title": "${testData.renamedTaskTitle}"}}}\n`);
},
responseHandler: () => {
executeNextStep();
}
},
summary: {
description: 'Test completion summary',
action: () => {
console.log('✅ All endpoint tests completed successfully!');
console.log('');
console.log('📊 Test Summary:');
console.log(` - Project ID used: ${testData.projectId || 'N/A'}`);
console.log(` - Created project: ${testData.createdProjectId || 'N/A'}`);
console.log(` - Task ID tested: ${testData.taskId || 'N/A'}`);
console.log(` - Task title: ${testData.taskTitle}`);
console.log('');
console.log('🎯 Agent-Friendly Endpoints Tested:');
console.log(' ✅ set_task_status - Change task status by title');
console.log(' ✅ complete_task - Mark task done by title');
console.log(' ✅ update_task_title - Change task title');
console.log(' ✅ update_task_description - Update task description');
console.log(' ✅ delete_task_by_title - Delete task by title');
console.log('');
console.log('🔧 Legacy Endpoints Tested:');
console.log(' ✅ update_task - Update task by ID (more complex)');
console.log(' ✅ get_task - Get task details by ID');
console.log('');
console.log('🎉 All MCP endpoints are working correctly!');
console.log('💡 Agents should prefer the title-based endpoints for easier usage');
setTimeout(() => mcpProcess.kill(), 500);
},
responseHandler: null
}
};
// Helper functions to determine context
function getListTasksContext() {
const prevSteps = testSequence.slice(0, currentStepIndex);
if (prevSteps[prevSteps.length - 1] === 'create_project') return 'empty';
if (prevSteps[prevSteps.length - 1] === 'set_task_status') return 'filtered';
if (prevSteps[prevSteps.length - 1] === 'complete_task') return 'completed';
if (prevSteps[prevSteps.length - 1] === 'update_task_description') return 'after updates';
if (prevSteps[prevSteps.length - 1] === 'delete_task_by_title') return 'final';
return 'with task';
}
function getCreateTaskContext() {
const prevSteps = testSequence.slice(0, currentStepIndex);
const createTaskCount = prevSteps.filter(step => step === 'create_task').length;
return createTaskCount > 0 ? 'second' : 'first';
}
// Execute current step
function executeCurrentStep() {
if (currentStepIndex >= testSequence.length) {
console.log('⚠️ All steps completed');
return;
}
const stepName = testSequence[currentStepIndex];
const stepHandler = stepHandlers[stepName];
if (!stepHandler) {
console.error(`❌ Unknown step: ${stepName}`);
return;
}
console.log(`🔄 Step ${currentStepIndex + 1}/${testSequence.length}: ${stepHandler.description}`);
setTimeout(() => {
stepHandler.action();
}, 100);
}
// Move to next step
function executeNextStep() {
currentStepIndex++;
executeCurrentStep();
}
// Start MCP process
const mcpProcess = spawn('vibe-kanban', ["--mcp"], {
stdio: ['pipe', 'pipe', 'inherit'],
});
mcpProcess.stdout.on('data', (data) => {
const response = data.toString().trim();
const currentStepName = testSequence[currentStepIndex];
const stepHandler = stepHandlers[currentStepName];
console.log(`📥 MCP Response (${currentStepName}):`);
console.log(response);
if (stepHandler?.responseHandler) {
stepHandler.responseHandler(response);
}
});
mcpProcess.on('exit', (code) => {
console.error(`🔴 MCP server exited with code: ${code}`);
process.exit(0);
});
mcpProcess.on('error', (error) => {
console.error('❌ MCP server error:', error);
process.exit(1);
});
// Start the sequence
setTimeout(() => {
executeCurrentStep();
}, 500);
// Safety timeout
setTimeout(() => {
console.error('⏰ Test timeout - killing process');
mcpProcess.kill();
}, 45000);

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env node #!/usr/bin/env node
const { execSync } = require("child_process"); const { execSync, spawn } = require("child_process");
const path = require("path"); const path = require("path");
const fs = require("fs"); const fs = require("fs");
@@ -19,9 +19,7 @@ function getPlatformDir() {
} else if (platform === "darwin" && arch === "arm64") { } else if (platform === "darwin" && arch === "arm64") {
return "macos-arm64"; return "macos-arm64";
} else { } else {
console.error( console.error(`❌ Unsupported platform: ${platform}-${arch}`);
`❌ Unsupported platform: ${platform}-${arch}`
);
console.error("Supported platforms:"); console.error("Supported platforms:");
console.error(" - Linux x64"); console.error(" - Linux x64");
console.error(" - Windows x64"); console.error(" - Windows x64");
@@ -31,64 +29,128 @@ function getPlatformDir() {
} }
} }
function getBinaryName() { function getBinaryName(base_name) {
return platform === "win32" ? "vibe-kanban.exe" : "vibe-kanban"; return platform === "win32" ? `${base_name}.exe` : base_name;
} }
try { const platformDir = getPlatformDir();
const platformDir = getPlatformDir(); const extractDir = path.join(__dirname, "..", "dist", platformDir);
const extractDir = path.join(__dirname, "..", "dist", platformDir);
const zipName = "vibe-kanban.zip"; const isMcpMode = process.argv.includes("--mcp");
if (!fs.existsSync(extractDir)) {
fs.mkdirSync(extractDir, { recursive: true });
}
if (isMcpMode) {
const baseName = "vibe-kanban-mcp";
const binaryName = getBinaryName(baseName);
const binaryPath = path.join(extractDir, binaryName);
const zipName = `${baseName}.zip`;
const zipPath = path.join(extractDir, zipName); const zipPath = path.join(extractDir, zipName);
// Check if binary exists, delete if it does
if (fs.existsSync(binaryPath)) {
fs.unlinkSync(binaryPath);
}
// Check if zip file exists // Check if zip file exists
if (!fs.existsSync(zipPath)) { if (!fs.existsSync(zipPath)) {
console.error(`❌ vibe-kanban.zip not found at: ${zipPath}`); // console.error(`❌ ${zipName} not found at: ${zipPath}`);
// console.error(`Current platform: ${platform}-${arch} (${platformDir})`);
process.exit(1);
}
// Unzip the file
// console.log(`📦 Extracting ${baseName}...`);
if (platform === "win32") {
// Use PowerShell on Windows
execSync(
`powershell -Command "Expand-Archive -Path '${zipPath}' -DestinationPath '${extractDir}' -Force"`,
{ stdio: "inherit" }
);
} else {
// Use unzip on Unix-like systems
execSync(`unzip -qq -o "${zipPath}" -d "${extractDir}"`, {
stdio: "inherit",
});
}
// Make sure it's executable
try {
fs.chmodSync(binaryPath, 0o755);
} catch (error) {
// console.error(
// "⚠️ Warning: Could not set executable permissions:",
// error.message
// );
}
// Launch MCP server
// console.error(`🚀 Starting ${baseName}...`);
const mcpProcess = spawn(binaryPath, [], {
stdio: ["pipe", "pipe", "inherit"], // stdin/stdout for MCP, stderr for logs
});
// Forward stdin to MCP server
process.stdin.pipe(mcpProcess.stdin);
// Forward MCP server stdout to our stdout
mcpProcess.stdout.pipe(process.stdout);
// Handle process termination
mcpProcess.on("exit", (code) => {
process.exit(code || 0);
});
mcpProcess.on("error", (error) => {
console.error("❌ MCP server error:", error.message);
process.exit(1);
});
// Handle Ctrl+C
process.on("SIGINT", () => {
console.error("\n🛑 Shutting down MCP server...");
mcpProcess.kill("SIGINT");
});
process.on("SIGTERM", () => {
mcpProcess.kill("SIGTERM");
});
} else {
const baseName = "vibe-kanban";
const binaryName = getBinaryName(baseName);
const binaryPath = path.join(extractDir, binaryName);
const zipName = `${baseName}.zip`;
const zipPath = path.join(extractDir, zipName);
// Check if binary exists, delete if it does
if (fs.existsSync(binaryPath)) {
fs.unlinkSync(binaryPath);
}
// Check if zip file exists
if (!fs.existsSync(zipPath)) {
console.error(`${zipName} not found at: ${zipPath}`);
console.error(`Current platform: ${platform}-${arch} (${platformDir})`); console.error(`Current platform: ${platform}-${arch} (${platformDir})`);
process.exit(1); process.exit(1);
} }
// Clean out any previous extraction (but keep the zip)
console.log("🧹 Cleaning up old files…");
if (fs.existsSync(extractDir)) {
fs.readdirSync(extractDir).forEach((name) => {
if (name !== zipName) {
fs.rmSync(path.join(extractDir, name), { recursive: true, force: true });
}
});
}
// Unzip the file // Unzip the file
console.log("📦 Extracting vibe-kanban..."); console.log(`📦 Extracting ${baseName}...`);
if (platform === "win32") { if (platform === "win32") {
// Use PowerShell on Windows // Use PowerShell on Windows
execSync(`powershell -Command "Expand-Archive -Path '${zipPath}' -DestinationPath '${extractDir}' -Force"`, { stdio: "inherit" }); execSync(
`powershell -Command "Expand-Archive -Path '${zipPath}' -DestinationPath '${extractDir}' -Force"`,
{ stdio: "inherit" }
);
} else { } else {
// Use unzip on Unix-like systems // Use unzip on Unix-like systems
execSync(`unzip -o "${zipPath}" -d "${extractDir}"`, { stdio: "inherit" }); execSync(`unzip -o "${zipPath}" -d "${extractDir}"`, { stdio: "inherit" });
} }
// Find the extracted directory (should match the zip structure) console.log(`🚀 Launching ${baseName}...`);
const extractedDirs = fs.readdirSync(extractDir).filter(name =>
name !== zipName && fs.statSync(path.join(extractDir, name)).isDirectory()
);
if (extractedDirs.length === 0) {
console.error("❌ No extracted directory found");
process.exit(1);
}
// Execute the binary
const binaryName = getBinaryName();
const binaryPath = path.join(extractDir, extractedDirs[0], binaryName);
if (!fs.existsSync(binaryPath)) {
console.error(`❌ Binary not found at: ${binaryPath}`);
process.exit(1);
}
console.log(`🚀 Launching vibe-kanban (${platformDir})...`);
if (platform === "win32") { if (platform === "win32") {
execSync(`"${binaryPath}"`, { stdio: "inherit" }); execSync(`"${binaryPath}"`, { stdio: "inherit" });
} else { } else {
@@ -96,7 +158,4 @@ try {
execSync(`chmod +x "${binaryPath}"`); execSync(`chmod +x "${binaryPath}"`);
execSync(`"${binaryPath}"`, { stdio: "inherit" }); execSync(`"${binaryPath}"`, { stdio: "inherit" });
} }
} catch (error) {
console.error("❌ Error running vibe-kanban:", error.message);
process.exit(1);
} }

View File

@@ -1,16 +1,17 @@
{ {
"name": "vibe-kanban", "name": "vibe-kanban",
"private": false, "private": false,
"version": "0.0.1", "version": "0.0.24",
"main": "index.js", "main": "index.js",
"bin": { "bin": {
"my-npx-cli": "bin/cli.js" "vibe-kanban": "bin/cli.js"
}, },
"keywords": [], "keywords": [],
"author": "bloop", "author": "bloop",
"license": "", "license": "",
"description": "NPX wrapper around vibe-kanban", "description": "NPX wrapper around vibe-kanban and vibe-kanban-mcp",
"files": [ "files": [
"dist" "dist",
"bin"
] ]
} }

View File

@@ -4,8 +4,10 @@
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "concurrently \"cargo watch -w backend -x 'run --manifest-path backend/Cargo.toml'\" \"npm run frontend:dev\"", "dev": "concurrently \"cargo watch -w backend -x 'run --manifest-path backend/Cargo.toml'\" \"npm run frontend:dev\"",
"build": "npm run frontend:build && cargo build --release --manifest-path backend/Cargo.toml", "build": "npm run frontend:build && cargo build --release --manifest-path backend/Cargo.toml && cargo build --release --bin mcp_task_server --manifest-path backend/Cargo.toml",
"build:single": "npm run frontend:build && cargo build --release --manifest-path backend/Cargo.toml", "build:single": "npm run frontend:build && cargo build --release --manifest-path backend/Cargo.toml",
"build:npm": "./build-npm-package.sh",
"test:npm": "./test-npm-package.sh",
"frontend:dev": "cd frontend && npm run dev", "frontend:dev": "cd frontend && npm run dev",
"frontend:build": "cd frontend && npm run build", "frontend:build": "cd frontend && npm run build",
"backend:dev": "cargo watch -w backend -x 'run --manifest-path backend/Cargo.toml'", "backend:dev": "cargo watch -w backend -x 'run --manifest-path backend/Cargo.toml'",

45
test-npm-package.sh Executable file
View File

@@ -0,0 +1,45 @@
#!/bin/bash
# test-npm-package.sh
set -e
echo "🧪 Testing NPM package locally..."
# Build the package first
./build-npm-package.sh
cd npx-cli
echo "📋 Checking files to be included..."
npm pack --dry-run
echo "📦 Creating package tarball..."
npm pack
echo "🔗 Installing globally from tarball..."
TARBALL=$(ls vibe-kanban-*.tgz | head -n1)
npm install -g "./$TARBALL"
echo "🧪 Testing main command..."
vibe-kanban &
MAIN_PID=$!
sleep 3
kill $MAIN_PID 2>/dev/null || true
wait $MAIN_PID 2>/dev/null || true
echo "✅ Main app started successfully"
echo "🧪 Testing MCP command with complete handshake..."
node ../mcp_test.js
echo "🧹 Cleaning up..."
npm uninstall -g vibe-kanban
rm "$TARBALL"
echo "✅ NPM package test completed successfully!"
echo ""
echo "🎉 Your MCP server is working correctly!"
echo "📋 Next steps:"
echo " 1. cd npx-cli"
echo " 2. npm publish"
echo " 3. Users can then use: npx vibe-kanban --mcp with Claude Desktop"