Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
53e032e
feat: add kanban-domain, kanban-core, uuid, chrono, tempfile deps to …
fulsomenko Feb 2, 2026
c86b588
feat: replace async CliExecutor with sync SyncExecutor
fulsomenko Feb 2, 2026
92019e0
feat: add McpContext implementing KanbanOperations trait
fulsomenko Feb 2, 2026
f9c09d2
feat: remove McpTools trait, replaced by KanbanOperations from kanban…
fulsomenko Feb 2, 2026
102a950
feat: rewrite MCP server with 37 tools via KanbanOperations trait
fulsomenko Feb 2, 2026
c7142d9
feat: add --clear-wip-limit flag to CLI column update
fulsomenko Feb 2, 2026
4088d89
feat: add sprint update fields to CLI (name, dates, clear flags)
fulsomenko Feb 2, 2026
10629f8
fix: error handling in MCP executor
fulsomenko Feb 2, 2026
e0488b1
feat: bring MCP context to full parity with CLI
fulsomenko Feb 2, 2026
5964d06
feat: update MCP server tools for full CLI parity
fulsomenko Feb 2, 2026
642abf5
test: add unit tests for MCP helpers and ArgsBuilder
fulsomenko Feb 2, 2026
ec2e892
test: add integration tests for MCP round-trips
fulsomenko Feb 2, 2026
95e82e0
chore: cargo fmt
fulsomenko Feb 2, 2026
dce1ab1
chore: add changeset
fulsomenko Feb 2, 2026
0e49d71
fix: remove MCP trait parity bypasses and broken clear flags
fulsomenko Feb 2, 2026
d9835ff
refactor: remove 4 dead pre-animation functions from TUI card_handlers
fulsomenko Feb 2, 2026
d7176f8
chore: update changeset
fulsomenko Feb 2, 2026
513a190
fix: build kanban from source in Nix and support KANBAN_BIN in tests
fulsomenko Feb 2, 2026
3e7c422
docs: add MCP server to README quick start
fulsomenko Feb 2, 2026
bde42e2
fix: add Display impls for CardPriority/CardStatus, replace fragile D…
fulsomenko Feb 2, 2026
1c422cf
fix: use parking_lot::Mutex to prevent poisoning, map errors to prope…
fulsomenko Feb 2, 2026
9157fe1
fix: add 30s timeout to CLI subprocess, fix multiline stderr parsing
fulsomenko Feb 2, 2026
cf9d3c9
refactor: move sprint name→index conversion into UpdateSprint command
fulsomenko Feb 2, 2026
508efc2
feat: extend create_card with optional fields, remove two-phase creation
fulsomenko Feb 2, 2026
2839010
chore: cargo fmt
fulsomenko Feb 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
bump: patch
---

- test: add integration tests for MCP round-trips
- test: add unit tests for MCP helpers and ArgsBuilder
- feat: update MCP server tools for full CLI parity
- feat: bring MCP context to full parity with CLI
- fix: error handling in MCP executor
- feat: add sprint update fields to CLI (name, dates, clear flags)
- feat: add --clear-wip-limit flag to CLI column update
- feat: rewrite MCP server with 37 tools via KanbanOperations trait
- feat: remove McpTools trait, replaced by KanbanOperations from kanban-domain
- feat: add McpContext implementing KanbanOperations trait
- feat: replace async CliExecutor with sync SyncExecutor
- feat: add kanban-domain, kanban-core, uuid, chrono, tempfile deps to kanban-mcp
- fix: remove create_card_full bypass, use trait two-step create+update pattern
- fix: remove update_sprint_full bypass, route through trait's update_sprint
- feat: add name field to SprintUpdate for MCP name passthrough
- fix: remove broken clear_description and clear_points MCP flags
- refactor: remove 4 dead pre-animation functions from TUI card_handlers
6 changes: 6 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,14 @@ kanban myboard.json # Load a board from file
3. Add cards with `n` and organize them
4. Press `x` to export as JSON

### MCP Server

```bash
nix run github:fulsomenko/kanban#kanban-mcp
```

Provides full read/write access to your boards, cards, columns, and sprints over the [Model Context Protocol](https://modelcontextprotocol.io) for use with LLM tools like Claude Code, Cursor, etc.

### CLI

```bash
Expand Down
12 changes: 12 additions & 0 deletions crates/kanban-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,8 @@ pub struct ColumnUpdateArgs {
pub position: Option<i32>,
#[arg(long)]
pub wip_limit: Option<u32>,
#[arg(long)]
pub clear_wip_limit: bool,
}

// Card commands
Expand Down Expand Up @@ -343,9 +345,19 @@ pub struct SprintUpdateArgs {
/// Sprint ID to update
pub id: Uuid,
#[arg(long)]
pub name: Option<String>,
#[arg(long)]
pub prefix: Option<String>,
#[arg(long)]
pub card_prefix: Option<String>,
#[arg(long)]
pub start_date: Option<String>,
#[arg(long)]
pub end_date: Option<String>,
#[arg(long)]
pub clear_start_date: bool,
#[arg(long)]
pub clear_end_date: bool,
}

// Export/Import commands
Expand Down
2 changes: 2 additions & 0 deletions crates/kanban-cli/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,7 @@ impl KanbanOperations for CliContext {
board_id: Uuid,
column_id: Uuid,
title: String,
options: kanban_domain::CreateCardOptions,
) -> KanbanResult<Card> {
use kanban_domain::commands::CreateCard;
let position = self
Expand All @@ -288,6 +289,7 @@ impl KanbanOperations for CliContext {
column_id,
title,
position,
options,
};
self.execute(Box::new(cmd))?;
self.cards.last().cloned().ok_or_else(|| {
Expand Down
50 changes: 13 additions & 37 deletions crates/kanban-cli/src/handlers/card.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,15 @@ use crate::cli::{CardAction, CardCreateArgs, CardListArgs, CardUpdateArgs};
use crate::context::CliContext;
use crate::output;
use kanban_domain::{
CardListFilter, CardPriority, CardStatus, CardSummary, CardUpdate, FieldUpdate,
KanbanOperations,
CardListFilter, CardPriority, CardStatus, CardSummary, CardUpdate, CreateCardOptions,
FieldUpdate, KanbanOperations,
};

pub async fn handle(ctx: &mut CliContext, action: CardAction) -> anyhow::Result<()> {
match action {
CardAction::Create(args) => {
let title = args.title.clone();
let mut card = ctx.create_card(args.board_id, args.column_id, title)?;

if args.description.is_some()
|| args.priority.is_some()
|| args.points.is_some()
|| args.due_date.is_some()
{
let updates =
build_card_update_from_create(&args).map_err(|e| anyhow::anyhow!(e))?;
card = ctx.update_card(card.id, updates)?;
}

let options = build_create_options(&args).map_err(|e| anyhow::anyhow!(e))?;
let card = ctx.create_card(args.board_id, args.column_id, args.title, options)?;
ctx.save().await?;
output::output_success(&card);
}
Expand Down Expand Up @@ -135,33 +124,20 @@ fn build_filter(args: &CardListArgs) -> Result<CardListFilter, String> {
})
}

fn build_card_update_from_create(args: &CardCreateArgs) -> Result<CardUpdate, String> {
fn build_create_options(args: &CardCreateArgs) -> Result<CreateCardOptions, String> {
let priority = match &args.priority {
Some(p) => Some(parse_priority(p)?),
None => None,
};
Ok(CardUpdate {
title: None,
description: args
.description
.clone()
.map(FieldUpdate::Set)
.unwrap_or(FieldUpdate::NoChange),
let due_date = match &args.due_date {
Some(d) => Some(parse_datetime(d)?),
None => None,
};
Ok(CreateCardOptions {
description: args.description.clone(),
priority,
status: None,
position: None,
column_id: None,
points: args
.points
.map(FieldUpdate::Set)
.unwrap_or(FieldUpdate::NoChange),
due_date: match &args.due_date {
Some(d) => FieldUpdate::Set(parse_datetime(d)?),
None => FieldUpdate::NoChange,
},
sprint_id: FieldUpdate::NoChange,
assigned_prefix: FieldUpdate::NoChange,
card_prefix: FieldUpdate::NoChange,
points: args.points,
due_date,
})
}

Expand Down
13 changes: 8 additions & 5 deletions crates/kanban-cli/src/handlers/column.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,14 @@ async fn handle_update(
let updates = ColumnUpdate {
name: args.name,
position: args.position,
wip_limit: args
.wip_limit
.map(|w| w as i32)
.map(FieldUpdate::Set)
.unwrap_or(FieldUpdate::NoChange),
wip_limit: if args.clear_wip_limit {
FieldUpdate::Clear
} else {
args.wip_limit
.map(|w| w as i32)
.map(FieldUpdate::Set)
.unwrap_or(FieldUpdate::NoChange)
},
};
let column = ctx.update_column(args.id, updates)?;
ctx.save().await?;
Expand Down
40 changes: 38 additions & 2 deletions crates/kanban-cli/src/handlers/sprint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,23 @@ use crate::context::CliContext;
use crate::output;
use kanban_domain::{FieldUpdate, KanbanOperations, SprintUpdate};

fn parse_datetime(s: &str) -> Result<chrono::DateTime<chrono::Utc>, String> {
chrono::DateTime::parse_from_rfc3339(s)
.map(|dt| dt.with_timezone(&chrono::Utc))
.or_else(|_| {
chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d")
.map_err(|_| ())
.and_then(|d| d.and_hms_opt(0, 0, 0).ok_or(()))
.map(|dt| dt.and_utc())
})
.map_err(|_| {
format!(
"Invalid date '{}'. Supported formats: YYYY-MM-DD or RFC 3339 (e.g., 2024-01-15T10:30:00Z)",
s
)
})
}

pub async fn handle(ctx: &mut CliContext, action: SprintAction) -> anyhow::Result<()> {
match action {
SprintAction::Create {
Expand Down Expand Up @@ -54,7 +71,26 @@ async fn handle_update(
ctx: &mut CliContext,
args: SprintUpdateArgs,
) -> anyhow::Result<kanban_domain::Sprint> {
let start_date = if args.clear_start_date {
FieldUpdate::Clear
} else {
match args.start_date {
Some(d) => FieldUpdate::Set(parse_datetime(&d).map_err(anyhow::Error::msg)?),
None => FieldUpdate::NoChange,
}
};

let end_date = if args.clear_end_date {
FieldUpdate::Clear
} else {
match args.end_date {
Some(d) => FieldUpdate::Set(parse_datetime(&d).map_err(anyhow::Error::msg)?),
None => FieldUpdate::NoChange,
}
};

let updates = SprintUpdate {
name: args.name,
name_index: FieldUpdate::NoChange,
prefix: args
.prefix
Expand All @@ -65,8 +101,8 @@ async fn handle_update(
.map(FieldUpdate::Set)
.unwrap_or(FieldUpdate::NoChange),
status: None,
start_date: FieldUpdate::NoChange,
end_date: FieldUpdate::NoChange,
start_date,
end_date,
};
let sprint = ctx.update_sprint(args.id, updates)?;
ctx.save().await?;
Expand Down
31 changes: 31 additions & 0 deletions crates/kanban-domain/src/card.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::fmt;
use uuid::Uuid;

use crate::{board::Board, column::ColumnId, field_update::FieldUpdate, sprint::Sprint, SprintLog};
Expand All @@ -23,6 +24,28 @@ pub enum CardStatus {
Done,
}

impl fmt::Display for CardPriority {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::Low => write!(f, "low"),
Self::Medium => write!(f, "medium"),
Self::High => write!(f, "high"),
Self::Critical => write!(f, "critical"),
}
}
}

impl fmt::Display for CardStatus {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::Todo => write!(f, "todo"),
Self::InProgress => write!(f, "in_progress"),
Self::Blocked => write!(f, "blocked"),
Self::Done => write!(f, "done"),
}
}
}

/// Represents card lifecycle operation types.
/// Used for visual feedback during card operations.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
Expand Down Expand Up @@ -331,6 +354,14 @@ pub struct CardUpdate {
pub card_prefix: FieldUpdate<String>,
}

#[derive(Debug, Clone, Default)]
pub struct CreateCardOptions {
pub description: Option<String>,
pub priority: Option<CardPriority>,
pub points: Option<u8>,
pub due_date: Option<DateTime<Utc>>,
}

impl GraphNode for Card {
fn node_id(&self) -> Uuid {
self.id
Expand Down
34 changes: 33 additions & 1 deletion crates/kanban-domain/src/commands/card_commands.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use super::{Command, CommandContext};
use crate::dependencies::card_graph::CardGraphExt;
use crate::CardUpdate;
use crate::{CardUpdate, CreateCardOptions};
use chrono::Utc;
use kanban_core::KanbanResult;
use uuid::Uuid;
Expand Down Expand Up @@ -30,6 +30,7 @@ pub struct CreateCard {
pub column_id: Uuid,
pub title: String,
pub position: i32,
pub options: CreateCardOptions,
}

impl Command for CreateCard {
Expand All @@ -53,6 +54,37 @@ impl Command for CreateCard {
&prefix,
);
context.cards.push(card);

// Apply optional fields
if self.options.description.is_some()
|| self.options.priority.is_some()
|| self.options.points.is_some()
|| self.options.due_date.is_some()
{
if let Some(card) = context.cards.last_mut() {
let updates = CardUpdate {
description: self
.options
.description
.clone()
.map(crate::FieldUpdate::Set)
.unwrap_or(crate::FieldUpdate::NoChange),
priority: self.options.priority,
points: self
.options
.points
.map(crate::FieldUpdate::Set)
.unwrap_or(crate::FieldUpdate::NoChange),
due_date: self
.options
.due_date
.map(crate::FieldUpdate::Set)
.unwrap_or(crate::FieldUpdate::NoChange),
..Default::default()
};
card.update(updates);
}
}
}
Ok(())
}
Expand Down
19 changes: 18 additions & 1 deletion crates/kanban-domain/src/commands/sprint_commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,25 @@ pub struct UpdateSprint {

impl Command for UpdateSprint {
fn execute(&self, context: &mut CommandContext) -> KanbanResult<()> {
let mut updates = self.updates.clone();

if let Some(ref name) = updates.name {
let board_id = context
.sprints
.iter()
.find(|s| s.id == self.sprint_id)
.map(|s| s.board_id);

if let Some(board_id) = board_id {
if let Some(board) = context.boards.iter_mut().find(|b| b.id == board_id) {
let idx = board.add_sprint_name_at_used_index(name.clone());
updates.name_index = crate::FieldUpdate::Set(idx);
}
}
}

if let Some(sprint) = context.sprints.iter_mut().find(|s| s.id == self.sprint_id) {
sprint.update(self.updates.clone());
sprint.update(updates);
}
Ok(())
}
Expand Down
Loading