feat(deck): compact card/comment retrieval (summaries, filters, board overview)

Deck read tools returned too many tokens to be usable as boards grow — even
deck_get_stacks(description_max_length=1) exceeded the MCP token limit because
every card was fully serialized in list views.

- Add compact projection models (DeckCardSummary, DeckCommentSummary,
  StackOverview, BoardOverviewResponse) and a uniform detail="summary"|"full"
  knob (summary default) on deck_get_cards / get_stacks / get_stack /
  get_archived_stacks.
- Add pre-serialization filtering: status (open/done/archived/all), label,
  assigned_to.
- Add deck_get_board_overview: board title + label legend + stacks with
  compact card rows + counts in a single call.
- Compact comments: detail / message_max_length / newest-first order on
  deck_get_card_comments.
- Docs + unit/integration tests.

BREAKING CHANGE: deck list tools now default to detail="summary" and
status="open". The include_archived_cards parameter is replaced by status
(use status="all" to include archived cards); pass detail="full" to restore
the previous per-card shape.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Chris Coutinho
2026-06-01 16:17:46 +02:00
co-authored by Claude Opus 4.8
parent c662d57c4f
commit b11103064c
5 changed files with 830 additions and 142 deletions
+91 -3
View File
@@ -129,6 +129,44 @@ class DeckCard(BaseModel):
return validated_users
class DeckCardSummary(BaseModel):
"""Compact projection of a :class:`DeckCard` for list/overview views.
Drops the heavy fields that dominate token cost when many cards are
returned at once — the full ``description`` (kept only as a short
``descriptionPreview``), the nested ``labels``/``assignedUsers``/
``attachments`` objects (kept as flat title/uid lists + counts), and the
``etag``/``order``/``*Modified``/``createdAt``/``deletedAt``/``overdue``/
``type``/``owner`` bookkeeping fields. Fetch a single card with
``deck_get_card`` (or ``detail="full"``) when the full body is needed.
"""
id: int
title: str
stackId: int
archived: bool = False
duedate: Optional[datetime] = None
done: Optional[datetime] = None
labels: List[str] = Field(
default_factory=list, description="Label titles assigned to the card"
)
assignedUsers: List[str] = Field(
default_factory=list, description="UIDs of users assigned to the card"
)
attachmentCount: Optional[int] = Field(
default=None, description="Number of attachments on the card"
)
commentsUnread: Optional[int] = Field(
default=None, description="Number of unread comments (Deck exposes no total)"
)
hasDescription: bool = Field(
default=False, description="Whether the card has a non-empty description"
)
descriptionPreview: Optional[str] = Field(
default=None, description="Truncated preview of the card description"
)
class DeckStack(BaseModel):
id: int
title: str
@@ -136,7 +174,9 @@ class DeckStack(BaseModel):
order: int
deletedAt: int
lastModified: Optional[int] = None
cards: Optional[List[DeckCard]] = None
# Cards may be projected to DeckCardSummary when a tool is called with
# detail="summary" (the default for list tools).
cards: Optional[List[Union[DeckCard, DeckCardSummary]]] = None
etag: Optional[str] = Field(default=None, alias="ETag")
@@ -175,6 +215,20 @@ class DeckComment(BaseModel):
replyTo: Optional[Any] = None # Self-referencing, handle later if needed
class DeckCommentSummary(BaseModel):
"""Compact projection of a :class:`DeckComment` for list views.
Drops ``mentions``, ``actorType``, ``actorDisplayName`` and ``replyTo``,
keeping only the fields needed to read the conversation. Long messages are
truncated by the tool layer via ``message_max_length``.
"""
id: int
actorId: str
message: str
creationDateTime: datetime
class DeckSession(BaseModel):
token: str
@@ -220,6 +274,36 @@ class ListStacksResponse(BaseResponse):
total: int = Field(description="Total number of stacks")
class StackOverview(BaseModel):
"""A stack plus its compact card rows, for the board-overview tool."""
id: int = Field(description="Stack ID")
title: str = Field(description="Stack title")
order: Optional[int] = Field(default=None, description="Stack sort order")
card_count: int = Field(description="Number of cards returned for this stack")
cards: List[DeckCardSummary] = Field(
default_factory=list, description="Compact card rows in this stack"
)
class BoardOverviewResponse(BaseResponse):
"""Compact whole-board snapshot: board → stacks → summary card rows.
A single-call replacement for deck_get_board + deck_get_stacks that keeps
the response small enough to fit the token budget on large boards.
"""
board_id: int = Field(description="Board ID")
title: str = Field(description="Board title")
labels: List[str] = Field(
default_factory=list, description="Board label titles (legend)"
)
stacks: List[StackOverview] = Field(
default_factory=list, description="Stacks with compact card rows"
)
total_cards: int = Field(description="Total cards across all returned stacks")
class CreateStackResponse(BaseResponse):
"""Response model for stack creation."""
@@ -269,7 +353,9 @@ class CreateLabelResponse(BaseResponse):
class ListCardsResponse(BaseResponse):
"""Response model for listing deck cards."""
cards: list[DeckCard] = Field(description="List of deck cards")
cards: list[Union[DeckCard, DeckCardSummary]] = Field(
description="List of deck cards (summaries unless detail='full')"
)
total: int = Field(description="Total number of cards")
@@ -293,7 +379,9 @@ class LabelOperationResponse(StatusResponse):
class ListCardCommentsResponse(BaseResponse):
"""Response model for listing card comments."""
results: list[DeckComment] = Field(description="Card comments in this page")
results: list[Union[DeckComment, DeckCommentSummary]] = Field(
description="Card comments in this page (summaries unless detail='full')"
)
count: int = Field(
description=(
"Number of comments returned in this page (page size, not the "