From eb7ccc6585ed9b3e61dd423365f34d4a75f73a6a Mon Sep 17 00:00:00 2001 From: Flash Date: Fri, 3 Apr 2026 20:21:51 +0000 Subject: [PATCH] feat(planka): add tools for comments (list_comments, add_comment) --- server.py | 45 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 38 insertions(+), 7 deletions(-) diff --git a/server.py b/server.py index 874f57d..ec87013 100644 --- a/server.py +++ b/server.py @@ -1,5 +1,6 @@ import os import requests +import sys from typing import Any, List, Optional from mcp.server.fastmcp import FastMCP from dotenv import load_dotenv @@ -16,8 +17,6 @@ class PlankaClient: def __init__(self, url: str, token: str): self.url = url.rstrip("/") self.token = token - # Planka uses standard JWT tokens, but they don't always require "Bearer " in the header - # for all internal API calls, though standard is to use it. self.headers = {"Authorization": f"Bearer {token}"} def _get(self, endpoint: str, params: Optional[dict] = None) -> Any: @@ -32,16 +31,18 @@ class PlankaClient: def get_projects(self) -> List[dict]: data = self._get("projects") - # Handle Planka API response structure which often wraps items in 'items' if isinstance(data, dict) and "items" in data: return data["items"] return data def get_boards(self, project_id: str) -> List[dict]: - data = self._get(f"projects/{project_id}/boards") - if isinstance(data, dict) and "items" in data: - return data["items"] - return data + data = self._get(f"projects/{project_id}") + if isinstance(data, dict): + if "boards" in data: + return data["boards"] + if "included" in data and isinstance(data["included"], dict) and "boards" in data["included"]: + return data["included"]["boards"] + return [] def get_cards(self, board_id: str) -> List[dict]: data = self._get(f"boards/{board_id}/cards") @@ -57,6 +58,21 @@ class PlankaClient: "description": description }) + def get_actions(self, card_id: str) -> List[dict]: + # Planka comments are in the 'actions' endpoint for a card + data = self._get(f"cards/{card_id}/actions") + if isinstance(data, dict) and "items" in data: + return data["items"] + return data + + def create_comment(self, card_id: str, text: str) -> dict: + return self._post(f"cards/{card_id}/actions", { + "type": "commentCard", + "data": { + "text": text + } + }) + # Initialize client lazily client = None @@ -98,5 +114,20 @@ def create_card(board_id: str, list_id: str, name: str, description: str = "") - card = get_client().create_card(board_id, list_id, name, description) return f"Created card: {card['name']} (ID: {card['id']})" +@mcp.tool() +def list_comments(card_id: str) -> str: + """List all comments for a given card ID.""" + actions = get_client().get_actions(card_id) + comments = [a for a in actions if a.get('type') == 'commentCard'] + if not comments: + return "No comments found." + return "\n".join([f"- {c['data']['text']} (By: {c['userId']}, at: {c['createdAt']})" for c in comments]) + +@mcp.tool() +def add_comment(card_id: str, text: str) -> str: + """Add a comment to a card.""" + action = get_client().create_comment(card_id, text) + return f"Comment added to card {card_id}." + if __name__ == "__main__": mcp.run()