freelancer/flowboard
Boards starter for the Dodil Boards Challenge. FastAPI + DataK3 + appid + Models + MCP.
A remix template · 0 remixes · Remix freelancer/flowboard
app/boards/doors/mcp.py
"""The MCP door exposes board use cases as authenticated tools.
Tools act as the connected person's identity. They enforce the same
visibility and permission rules as the web API and never expose raw DB access.
"""
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 = (
"A team task board. Lists are 'To do', 'Doing' and 'Done'. "
"Use my_boards to find board IDs and show_board to inspect cards. "
"Use update_card to edit card details. Use activity_history to inspect "
"recent board changes. Viewers can read board history but cannot edit."
)
def build_mcp(boards: Boards) -> MCPServer:
server = MCPServer(
name="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 exc:
raise ToolError(str(exc)) from exc
@server.tool()
def my_boards(ctx: Context) -> list:
"""List the boards you can access and your role on each."""
return call(ctx, boards.my_boards)
@server.tool()
def show_board(board_id: str, ctx: Context) -> dict:
"""Show a board and its cards. Requires board visibility."""
return call(ctx, boards.show_board, board_id)
@server.tool()
def create_board(title: str, ctx: Context) -> dict:
"""Create a board. You become 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",
) -> dict:
"""Add a card to a board list."""
return call(
ctx,
boards.add_card,
board_id,
title,
list_name,
)
@server.tool()
def move_card(
card_id: str,
list_name: str,
ctx: Context,
) -> dict:
"""Move a card to 'To do', 'Doing', or 'Done'."""
return call(ctx, boards.move_card, card_id, list_name)
@server.tool()
def update_card(
card_id: str,
ctx: Context,
title: Optional[str] = None,
description: Optional[str] = None,
assignee: Optional[str] = None,
due_date: Optional[str] = None,
labels: Optional[list[str]] = None,
) -> dict:
"""Update card fields. Empty assignee or due_date clears that field."""
return call(
ctx,
boards.update_card,
card_id,
title=title,
description=description,
assignee=assignee,
due_date=due_date,
labels=labels,
)
@server.tool()
def activity_history(
board_id: str,
ctx: Context,
limit: int = 100,
) -> dict:
"""Get recent board activity, newest first. Limit must be 1–500."""
return call(
ctx,
boards.activity_history,
board_id,
limit,
)
@server.tool()
def add_member(
board_id: str,
email: str,
ctx: Context,
role: str = "member",
) -> dict:
"""Owner only: add someone as a 'member' or 'viewer'."""
return call(
ctx,
boards.add_member,
board_id,
email,
role,
)
@server.tool()
def suggest_checklist(
card_id: str,
ctx: Context,
) -> dict:
"""Suggest an AI checklist without changing the card."""
return call(ctx, boards.suggest_checklist, card_id)
return server