akif/arclet-boards

Arclet Boards — private Kanban workspaces with AppID, DataK3, shared web and MCP actions.

A remix template · 1 remixes · Remix akif/arclet-boards

app/mcp/server.py

"""Action tools share the exact validated contracts used by HTTP."""
import inspect
from pydantic_core import PydanticUndefined
from mcp.server.mcpserver import Context, MCPServer
from mcp.server.mcpserver.exceptions import ToolError

from app.application.contracts import READS, WRITES
from app.domain.model import AppError
from app.infrastructure.identity import person_from

DESCRIPTIONS = {
    "list_workspaces": "List workspaces you belong to, including workspaces with boards shared with you.",
    "get_workspace": "Get a workspace and only the boards you can see. Workspace owners can see its member directory.",
    "create_workspace": "Create a workspace owned by you. Reuse request_id when retrying the same command.",
    "invite_workspace_member": "Workspace owner only: grant workspace access. Does not grant access to private boards.",
    "list_boards": "List only your accessible boards, their ids, roles, and progress counts. Start here.",
    "get_board": "Read an accessible board's lists, cards, versions, labels and member directory.",
    "get_card": "Read an accessible card's full details, checklist and activity history.",
    "create_board": "Create a board in a workspace you can edit. You own the new board and its three starter lists.",
    "update_board": "Update an editable board. Use its current version; a stale version returns a conflict.",
    "create_list": "Create an ordered list on an editable board. Mark is_done for completed work.",
    "update_list": "Edit a list's name and completion semantics with its current version.",
    "reorder_lists": "Reorder all lists on a board. Supply each id exactly once and the current board version.",
    "add_card": "Create a card on an editable board and a list on that same board. Records creation history.",
    "update_card": "Edit card details and replace its labels with the given list. Current card version required.",
    "move_card": "Move/reorder a card atomically on its board. index is zero-based after removing that card; get fresh card and board versions first.",
    "assign_card": "Assign a card to an existing board member, or clear with null. Current card version required.",
    "set_due_date": "Set YYYY-MM-DD due date, or clear with null. Current card version required.",
    "list_my_cards": "List cards assigned to you, only across boards you can see, ordered by due date.",
    "list_overdue": "List overdue cards on your accessible boards. Completed lists are excluded; dates use UTC.",
    "get_card_activity": "Read a visible card's chronological audit trail with actor, time and before/after changes.",
    "list_members": "List a visible board's members and board-specific roles.",
    "invite_member": "Board owner only: grant a person access by email. They sign in through AppID; this does not send email or create an AppID account.",
    "change_member_role": "Board owner only: change an existing member to member or viewer. Ownership is immutable.",
    "remove_member": "Board owner only: remove a member and atomically clear their card assignments with history.",
    "add_checklist_item": "Add a manual checklist item to an editable card and record history.",
    "complete_checklist_item": "Complete or reopen a checklist item using its current version. Viewers cannot mutate.",
    "get_blocking_chain": "Explain a visible card's unfinished prerequisites and transitive blocking chains, using real card titles.",
    "add_dependency": "Link a same-board prerequisite to an editable card. Cycles and self-links are refused. Current card version required.",
    "remove_dependency": "Remove an editable card's prerequisite with history. Current card version required.",
    "suggest_checklist": "Ask Dodil Models to draft checklist steps from a visible card's title and description. Saves nothing; review before applying.",
    "apply_checklist": "Append reviewed checklist steps to an editable card in one transaction with history. Current card version required.",
    "daily_summary": "Summarize an accessible board's real counts and today's activity. Dodil Models selects focus only from existing unfinished card IDs.",
    "refresh_semantic_index": "Generate Dodil embeddings for changed cards on an editable board (up to 100 cards). Stores vectors in DataK3; model usage is billed by Dodil. Current board version required.",
    "find_similar_cards": "Find semantically similar visible cards on the same board via DataK3 vector search. Ignores stale vectors; needs_index signals an editor must refresh first.",
}


def build_mcp(boards):
    server = MCPServer(name="Arclet Boards", instructions=(
        "You act as the person signed in through AppID. Start with list_boards, then get_board. "
        "Use fresh versions for edits. Supply a unique request_id for each mutation and reuse it "
        "when retrying that exact command. Outsiders cannot discover private resources. "
        "Inviting a board member grants application access; it does not send an email."))

    def register(name, contract):
        write = name in WRITES

        def action(**arguments):
            context = arguments.pop("ctx")
            request_id = arguments.pop("request_id", None)
            person = person_from(context.headers or {})
            try:
                return boards.call(name, person, arguments, request_id)
            except AppError as error:
                raise ToolError(f"{error.status}: {error}") from None

        required, optional = [], []
        output = list[dict] if name in {"list_workspaces", "list_boards", "list_my_cards",
            "list_overdue", "get_card_activity", "list_members"} else dict
        annotations = {"ctx": Context, "return": output}
        for field_name, field in contract.model_fields.items():
            annotation = field.annotation
            annotations[field_name] = annotation
            param = inspect.Parameter(field_name, inspect.Parameter.POSITIONAL_OR_KEYWORD,
                annotation=annotation, default=inspect.Parameter.empty
                if field.default is PydanticUndefined else field.default)
            (required if field.is_required() else optional).append(param)
        if write:
            annotations["request_id"] = str
            required.append(inspect.Parameter("request_id", inspect.Parameter.POSITIONAL_OR_KEYWORD,
                                               annotation=str))
        required.append(inspect.Parameter("ctx", inspect.Parameter.POSITIONAL_OR_KEYWORD,
                                          annotation=Context))
        action.__name__, action.__doc__, action.__annotations__ = name, DESCRIPTIONS[name], annotations
        action.__signature__ = inspect.Signature(required + optional, return_annotation=output)
        server.tool()(action)

    for name, contract in (READS | WRITES).items():
        register(name, contract)
    return server