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

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

View File

@@ -1,4 +1,17 @@
use std::{fs, path::Path};
fn main() {
// Tell cargo to rerun build script if models change
println!("cargo:rerun-if-changed=src/models/");
// Create frontend/dist directory if it doesn't exist
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 executor;
pub mod executors;
pub mod mcp;
pub mod models;
pub mod routes;
pub mod utils;

View File

@@ -17,6 +17,7 @@ mod app_state;
mod execution_monitor;
mod executor;
mod executors;
mod mcp;
mod models;
mod routes;
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?;
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
}
}