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