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:
committed by
GitHub
parent
58f621c816
commit
0514d437a2
2
.gitignore
vendored
2
.gitignore
vendored
@@ -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
|
||||||
56
backend/.sqlx/query-7193dead2b112b137880482fe8e8c822c67ef6692e0456683331a438a4aa002f.json
generated
Normal file
56
backend/.sqlx/query-7193dead2b112b137880482fe8e8c822c67ef6692e0456683331a438a4aa002f.json
generated
Normal 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"
|
||||||
|
}
|
||||||
@@ -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"] }
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
34
backend/src/bin/mcp_task_server.rs
Normal file
34
backend/src/bin/mcp_task_server.rs
Normal 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(())
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
1
backend/src/mcp/mod.rs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
pub mod task_server;
|
||||||
1333
backend/src/mcp/task_server.rs
Normal file
1333
backend/src/mcp/task_server.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
33
build-npm-package.sh
Executable 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
374
mcp_test.js
Normal 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);
|
||||||
@@ -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);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
45
test-npm-package.sh
Executable 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"
|
||||||
Reference in New Issue
Block a user