feat(planka): add tools for comments (list_comments, add_comment)

This commit is contained in:
Flash
2026-04-03 20:21:51 +00:00
parent 7943e8a3da
commit eb7ccc6585

View File

@@ -1,5 +1,6 @@
import os import os
import requests import requests
import sys
from typing import Any, List, Optional from typing import Any, List, Optional
from mcp.server.fastmcp import FastMCP from mcp.server.fastmcp import FastMCP
from dotenv import load_dotenv from dotenv import load_dotenv
@@ -16,8 +17,6 @@ class PlankaClient:
def __init__(self, url: str, token: str): def __init__(self, url: str, token: str):
self.url = url.rstrip("/") self.url = url.rstrip("/")
self.token = token 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}"} self.headers = {"Authorization": f"Bearer {token}"}
def _get(self, endpoint: str, params: Optional[dict] = None) -> Any: def _get(self, endpoint: str, params: Optional[dict] = None) -> Any:
@@ -32,16 +31,18 @@ class PlankaClient:
def get_projects(self) -> List[dict]: def get_projects(self) -> List[dict]:
data = self._get("projects") data = self._get("projects")
# Handle Planka API response structure which often wraps items in 'items'
if isinstance(data, dict) and "items" in data: if isinstance(data, dict) and "items" in data:
return data["items"] return data["items"]
return data return data
def get_boards(self, project_id: str) -> List[dict]: def get_boards(self, project_id: str) -> List[dict]:
data = self._get(f"projects/{project_id}/boards") data = self._get(f"projects/{project_id}")
if isinstance(data, dict) and "items" in data: if isinstance(data, dict):
return data["items"] if "boards" in data:
return 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]: def get_cards(self, board_id: str) -> List[dict]:
data = self._get(f"boards/{board_id}/cards") data = self._get(f"boards/{board_id}/cards")
@@ -57,6 +58,21 @@ class PlankaClient:
"description": description "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 # Initialize client lazily
client = None 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) card = get_client().create_card(board_id, list_id, name, description)
return f"Created card: {card['name']} (ID: {card['id']})" 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__": if __name__ == "__main__":
mcp.run() mcp.run()