feat(mcp): mint-token CLI + get_card / delete_comment tools + executeToolAs(actor)
Net-new capacidades recuperadas del WIP stash que el merge notif no traia: - mint-token CLI subcommand: 'kanban mint-token --user <id> --name <pc>' genera token bearer para configurar Claude Code u otros clientes MCP HTTP sin tocar la UI. - executeToolAs(db, name, input, actor): variante actor-aware de executeTool. El dispatcher HTTP /mcp pasa el user_id resuelto del bearer token; tools per-user (add_comment, delete_comment) lo usan como autor sin que el llamante pueda forjarlo. - get_card tool: lookup por id o seq_num. Devuelve Card completa. - delete_comment tool: borra card_message; solo el autor original (validado en DB). executeTool() sigue siendo el wrapper legacy sin actor para chat WS.
This commit is contained in:
+81
-12
@@ -19,8 +19,15 @@ func errResult(err error) ToolResult { return ToolResult{OK: false, Error: err.
|
||||
func errMsg(msg string) ToolResult { return ToolResult{OK: false, Error: msg} }
|
||||
|
||||
// executeTool dispatches a tool by name with raw JSON input and returns a ToolResult.
|
||||
// Tools that mutate the board return ok=true on success; read-only tools include their data in result.
|
||||
// Used by the legacy chat path (no authenticated user available).
|
||||
func executeTool(db *DB, name string, input json.RawMessage) ToolResult {
|
||||
return executeToolAs(db, name, input, "")
|
||||
}
|
||||
|
||||
// executeToolAs is the actor-aware dispatch used by the HTTP MCP path.
|
||||
// actor is the authenticated user id (resolved from the bearer token) for tools
|
||||
// that need it (add_comment / delete_comment infer the author from it).
|
||||
func executeToolAs(db *DB, name string, input json.RawMessage, actor string) ToolResult {
|
||||
switch name {
|
||||
case "list_board":
|
||||
return toolListBoard(db)
|
||||
@@ -50,10 +57,14 @@ func executeTool(db *DB, name string, input json.RawMessage) ToolResult {
|
||||
return toolListUsers(db)
|
||||
case "assign_card":
|
||||
return toolAssignCard(db, input)
|
||||
case "get_card":
|
||||
return toolGetCard(db, input)
|
||||
case "add_comment":
|
||||
return toolAddComment(db, input)
|
||||
return toolAddCommentAs(db, input, actor)
|
||||
case "list_comments":
|
||||
return toolListComments(db, input)
|
||||
case "delete_comment":
|
||||
return toolDeleteComment(db, input, actor)
|
||||
default:
|
||||
return errMsg("unknown tool: " + name)
|
||||
}
|
||||
@@ -64,7 +75,7 @@ func toolMutates(name string) bool {
|
||||
switch name {
|
||||
case "create_column", "update_column", "rename_column", "delete_column", "reorder_columns",
|
||||
"create_card", "update_card", "delete_card", "move_card", "assign_card",
|
||||
"add_comment":
|
||||
"add_comment", "delete_comment":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@@ -352,7 +363,8 @@ func validateToolName(name string) error {
|
||||
"update_card": true, "delete_card": true, "move_card": true,
|
||||
"card_history": true, "find_cards": true,
|
||||
"list_users": true, "assign_card": true,
|
||||
"add_comment": true, "list_comments": true,
|
||||
"add_comment": true, "list_comments": true, "delete_comment": true,
|
||||
"get_card": true,
|
||||
}
|
||||
if !known[name] {
|
||||
return fmt.Errorf("unknown tool: %s", name)
|
||||
@@ -360,10 +372,15 @@ func validateToolName(name string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// toolAddComment appends a comment (card_message) to a card. Accepts either
|
||||
// {card_id, body, author_id} or {card_id, body, author_username}. Resolves
|
||||
// the username to an id when needed.
|
||||
func toolAddComment(db *DB, input json.RawMessage) ToolResult {
|
||||
// toolAddCommentAs appends a comment (card_message) to a card.
|
||||
//
|
||||
// Author resolution order:
|
||||
// 1. explicit "author_id" in input (legacy chat path)
|
||||
// 2. explicit "author_username" in input -> resolve to id
|
||||
// 3. fallback to `actor` (authenticated user from MCP HTTP token)
|
||||
//
|
||||
// At least one must yield a non-empty id.
|
||||
func toolAddCommentAs(db *DB, input json.RawMessage, actor string) ToolResult {
|
||||
var in struct {
|
||||
CardID string `json:"card_id"`
|
||||
Body string `json:"body"`
|
||||
@@ -380,16 +397,19 @@ func toolAddComment(db *DB, input json.RawMessage) ToolResult {
|
||||
return errMsg("body required")
|
||||
}
|
||||
authorID := strings.TrimSpace(in.AuthorID)
|
||||
if authorID == "" {
|
||||
if in.AuthorUsername == "" {
|
||||
return errMsg("author_id or author_username required")
|
||||
}
|
||||
if authorID == "" && in.AuthorUsername != "" {
|
||||
u, _, err := db.GetUserByUsername(in.AuthorUsername)
|
||||
if err != nil {
|
||||
return errResult(fmt.Errorf("author_username: %w", err))
|
||||
}
|
||||
authorID = u.ID
|
||||
}
|
||||
if authorID == "" {
|
||||
authorID = actor
|
||||
}
|
||||
if authorID == "" {
|
||||
return errMsg("author_id, author_username, or authenticated MCP token required")
|
||||
}
|
||||
m, err := db.CreateCardMessage(in.CardID, authorID, in.Body)
|
||||
if err != nil {
|
||||
return errResult(err)
|
||||
@@ -397,6 +417,55 @@ func toolAddComment(db *DB, input json.RawMessage) ToolResult {
|
||||
return okResult(m)
|
||||
}
|
||||
|
||||
// toolGetCard returns a single active (non-archived) card by id or seq_num.
|
||||
// Pass exactly ONE of {id, seq_num}.
|
||||
func toolGetCard(db *DB, input json.RawMessage) ToolResult {
|
||||
var in struct {
|
||||
ID string `json:"id"`
|
||||
SeqNum int `json:"seq_num"`
|
||||
}
|
||||
if err := json.Unmarshal(input, &in); err != nil {
|
||||
return errResult(err)
|
||||
}
|
||||
if in.ID == "" && in.SeqNum == 0 {
|
||||
return errMsg("provide id or seq_num")
|
||||
}
|
||||
cards, err := db.ListCardsWithTime()
|
||||
if err != nil {
|
||||
return errResult(err)
|
||||
}
|
||||
for _, c := range cards {
|
||||
if in.ID != "" && c.ID == in.ID {
|
||||
return okResult(c)
|
||||
}
|
||||
if in.SeqNum != 0 && c.SeqNum == in.SeqNum {
|
||||
return okResult(c)
|
||||
}
|
||||
}
|
||||
return errMsg("card not found")
|
||||
}
|
||||
|
||||
// toolDeleteComment deletes a comment. Only the original author can delete it
|
||||
// (enforced via actor == message.author_id).
|
||||
func toolDeleteComment(db *DB, input json.RawMessage, actor string) ToolResult {
|
||||
if actor == "" {
|
||||
return errMsg("authenticated user required (call via MCP HTTP with a valid token)")
|
||||
}
|
||||
var in struct {
|
||||
ID string `json:"id"`
|
||||
}
|
||||
if err := json.Unmarshal(input, &in); err != nil {
|
||||
return errResult(err)
|
||||
}
|
||||
if in.ID == "" {
|
||||
return errMsg("id required")
|
||||
}
|
||||
if err := db.DeleteCardMessage(in.ID, actor); err != nil {
|
||||
return errResult(err)
|
||||
}
|
||||
return okResult(map[string]bool{"ok": true})
|
||||
}
|
||||
|
||||
// toolListComments returns every comment (card_message) attached to a card
|
||||
// sorted by created_at ascending.
|
||||
func toolListComments(db *DB, input json.RawMessage) ToolResult {
|
||||
|
||||
Reference in New Issue
Block a user