contest/orbit-boards

Orbit Boards, a private, remixable team task board built with FastAPI, DataK3, AppID, Models and MCP.

A remix template · 0 remixes · Remix contest/orbit-boards

app/boards/doors/mcp.py

"""Action-oriented MCP tools. Every operation uses the same authorized application service."""
from typing import Optional

from mcp.server.mcpserver import Context, MCPServer
from mcp.server.mcpserver.exceptions import ToolError

from boards.application.use_cases import Boards, NotFound
from boards.doors.identity import person_from
from boards.domain.model import Forbidden, Invalid

INSTRUCTIONS = (
    "Orbit Boards is a private team task board. Start with my_boards to see only boards "
    "available to the signed-in person. Lists are To do, Doing and Done. Use action tools "
    "for changes; viewers can read but cannot modify."
)


def build_mcp(boards: Boards) -> MCPServer:
    server = MCPServer(name="orbit-boards", instructions=INSTRUCTIONS)

    def call(ctx: Context, fn, *args, **kwargs):
        person = person_from(ctx.headers or {})
        if person is None:
            raise ToolError("401: sign in first")
        try:
            return fn(person, *args, **kwargs)
        except (NotFound, Forbidden, Invalid) as error:
            raise ToolError(str(error))
        except ValueError as error:
            raise ToolError("invalid value") from error

    @server.tool()
    def list_boards(ctx: Context) -> list:
        """List boards available to the signed-in person and their role on each."""
        return call(ctx, boards.my_boards)

    @server.tool()
    def show_board(board_id: str, ctx: Context) -> dict:
        """Show one board and its cards, lists, labels, due dates and team."""
        return call(ctx, boards.show_board, board_id)

    @server.tool()
    def list_my_cards(ctx: Context, days: int = 7) -> list:
        """List your assigned, unfinished cards due within the next number of days, including overdue cards."""
        return call(ctx, boards.list_my_cards, days)

    @server.tool()
    def list_overdue(ctx: Context, board_id: Optional[str] = None) -> list:
        """List unfinished cards past their due date on visible boards, optionally limited to one board."""
        return call(ctx, boards.list_overdue, board_id)

    @server.tool()
    def create_board(title: str, ctx: Context) -> dict:
        """Create a new private board. The caller becomes its owner."""
        return call(ctx, boards.create_board, title)

    @server.tool()
    def add_card(board_id: str, title: str, ctx: Context, list_name: str = "To do",
                 description: str = "", assignee: Optional[str] = None,
                 due_date: Optional[str] = None, labels: Optional[list[str]] = None) -> dict:
        """Create a card on a visible board, optionally with description, assignee, due date and labels."""
        return call(ctx, boards.add_card, board_id, title, list_name, description,
                    assignee, due_date, labels)

    @server.tool()
    def update_card(card_id: str, ctx: Context, title: Optional[str] = None,
                    description: Optional[str] = None, due_date: Optional[str] = None,
                    labels: Optional[list[str]] = None) -> dict:
        """Edit card title, description, due date or labels. Omit fields that should stay unchanged."""
        changes = {key: value for key, value in {"title": title, "description": description,
                    "due_date": due_date, "labels": labels}.items() if value is not None}
        return call(ctx, boards.update_card, card_id, **changes)

    @server.tool()
    def move_card(card_id: str, list_name: str, ctx: Context, position: Optional[int] = None) -> dict:
        """Move or reorder a card in the same board. Valid lists are To do, Doing and Done."""
        return call(ctx, boards.move_card, card_id, list_name, position)

    @server.tool()
    def assign_card(card_id: str, email: Optional[str], ctx: Context) -> dict:
        """Assign a card to a board member by email, or clear its assignment with null."""
        return call(ctx, boards.assign_card, card_id, email)

    @server.tool()
    def get_activity(board_id: str, ctx: Context, limit: int = 50) -> list[dict]:
        """Read recent card activity on a board the caller can see."""
        return call(ctx, boards.get_activity, board_id, limit)

    @server.tool()
    def add_member(board_id: str, email: str, ctx: Context, role: str = "member") -> dict:
        """Owner only: add a person as member or read-only viewer to a board."""
        return call(ctx, boards.add_member, board_id, email, role)

    @server.tool()
    def suggest_checklist(card_id: str, ctx: Context) -> dict:
        """Ask Dodil Models for a suggested checklist. This does not change the card."""
        return call(ctx, boards.suggest_checklist, card_id)

    return server