Add granular control over MCP tool response sizes to minimize context
window usage while maintaining full functionality when needed.
## Output Control Parameters
### Read Operations (ready, list, show, blocked)
- `brief`: Return BriefIssue {id, title, status, priority} (~97% smaller)
- `fields`: Custom field projection with validation - invalid fields raise ValueError
- `max_description_length`: Truncate descriptions to N chars with "..."
- `brief_deps`: Full issue but dependencies as BriefDep (~48% smaller)
### Write Operations (create, update, close, reopen)
- `brief`: Return OperationResult {id, action, message} (~97% smaller)
- Default is brief=True for minimal confirmations
## Token Savings
| Operation | Before | After (brief) | Reduction |
|-----------|--------|---------------|-----------|
| ready(limit=10) | ~20KB | ~600B | 97% |
| list(limit=20) | ~40KB | ~800B | 98% |
| show() | ~2KB | ~60B | 97% |
| show() with 5 deps | ~4.5KB | ~2.3KB | 48% |
| blocked() | ~10KB | ~240B | 98% |
| create() response | ~2KB | ~50B | 97% |
## New Models
- BriefIssue: Ultra-minimal issue (4 fields, ~60B)
- BriefDep: Compact dependency (5 fields, ~70B)
- OperationResult: Write confirmation (3 fields, ~50B)
- OperationAction: Literal["created", "updated", "closed", "reopened"]
## Best Practices
- Unified `brief` parameter naming across all operations
- `brief=True` always means "give me less data"
- Field validation with clear error messages listing valid fields
- All parameters optional with backwards-compatible defaults
- Progressive disclosure: scan cheap, detail on demand
---
## Filtering Parameters (aligns MCP with CLI)
Add missing filter parameters to `list` and `ready` tools that were
documented in MCP instructions but not implemented.
### ReadyWorkParams - New Fields
- `labels: list[str]` - AND filter: must have ALL specified labels
- `labels_any: list[str]` - OR filter: must have at least one
- `unassigned: bool` - Filter to only unassigned issues
- `sort_policy: str` - Sort by: hybrid (default), priority, oldest
### ListIssuesParams - New Fields
- `labels: list[str]` - AND filter: must have ALL specified labels
- `labels_any: list[str]` - OR filter: must have at least one
- `query: str` - Search in title (case-insensitive substring)
- `unassigned: bool` - Filter to only unassigned issues
### CLI Flag Mappings
| MCP Parameter | ready CLI Flag | list CLI Flag |
|---------------|----------------|---------------|
| labels | --label (repeated) | --label (repeated) |
| labels_any | --label-any (repeated) | --label-any (repeated) |
| query | N/A | --title |
| unassigned | --unassigned | --no-assignee |
| sort_policy | --sort | N/A |
---
## Documentation & Testing
### get_tool_info() Updates
- Added `brief`, `brief_deps`, `fields`, `max_description_length` to show tool
- Added `brief` parameter docs for create, update, close, reopen
- Added `brief`, `brief_deps` parameter docs for blocked
### Test Coverage (16 new tests)
- `test_create_brief_default` / `test_create_brief_false`
- `test_update_brief_default` / `test_update_brief_false`
- `test_close_brief_default` / `test_close_brief_false`
- `test_reopen_brief_default`
- `test_show_brief` / `test_show_fields_projection` / `test_show_fields_invalid`
- `test_show_max_description_length` / `test_show_brief_deps`
- `test_list_brief` / `test_ready_brief` / `test_blocked_brief`
---
## Backward Compatibility
All new parameters are optional with sensible defaults:
- `brief`: False for reads, True for writes
- `fields`, `max_description_length`: None (no filtering/truncation)
- `labels`, `labels_any`, `query`: None (no filtering)
- `unassigned`: False (include all)
- `sort_policy`: None (use default hybrid sort)
Existing MCP tool calls continue to work unchanged.
265 lines
7.4 KiB
Python
265 lines
7.4 KiB
Python
"""Pydantic models for beads issue tracker types."""
|
|
|
|
from datetime import datetime
|
|
from typing import Literal, Any
|
|
|
|
from pydantic import BaseModel, Field, field_validator
|
|
|
|
# Type aliases for issue statuses, types, and dependencies
|
|
IssueStatus = Literal["open", "in_progress", "blocked", "deferred", "closed"]
|
|
IssueType = Literal["bug", "feature", "task", "epic", "chore"]
|
|
DependencyType = Literal["blocks", "related", "parent-child", "discovered-from"]
|
|
OperationAction = Literal["created", "updated", "closed", "reopened"]
|
|
|
|
|
|
# =============================================================================
|
|
# CONTEXT ENGINEERING: Minimal Models for List Views
|
|
# =============================================================================
|
|
# These lightweight models reduce context window usage by ~80% for list operations.
|
|
# Use full Issue model only when detailed information is needed (show command).
|
|
|
|
class IssueMinimal(BaseModel):
|
|
"""Minimal issue model for list views (~80% smaller than full Issue).
|
|
|
|
Use this for ready_work, list_issues, and other bulk operations.
|
|
For full details including dependencies, use Issue model via show().
|
|
"""
|
|
id: str
|
|
title: str
|
|
status: IssueStatus
|
|
priority: int = Field(ge=0, le=4)
|
|
issue_type: IssueType
|
|
assignee: str | None = None
|
|
labels: list[str] = Field(default_factory=list)
|
|
dependency_count: int = 0
|
|
dependent_count: int = 0
|
|
|
|
@field_validator("priority")
|
|
@classmethod
|
|
def validate_priority(cls, v: int) -> int:
|
|
if not 0 <= v <= 4:
|
|
raise ValueError("Priority must be between 0 and 4")
|
|
return v
|
|
|
|
|
|
class CompactedResult(BaseModel):
|
|
"""Result container for compacted list responses.
|
|
|
|
When results exceed threshold, returns preview + metadata instead of full data.
|
|
This prevents context window overflow for large issue lists.
|
|
"""
|
|
compacted: bool = True
|
|
total_count: int
|
|
preview: list[IssueMinimal]
|
|
preview_count: int
|
|
hint: str = "Use show(issue_id) for full issue details"
|
|
|
|
|
|
class BriefIssue(BaseModel):
|
|
"""Ultra-minimal issue for scanning (4 fields).
|
|
|
|
Use for quick scans where only identification + priority needed.
|
|
~95% smaller than full Issue.
|
|
"""
|
|
id: str
|
|
title: str
|
|
status: IssueStatus
|
|
priority: int = Field(ge=0, le=4)
|
|
|
|
|
|
class BriefDep(BaseModel):
|
|
"""Brief dependency for overview (5 fields).
|
|
|
|
Use with brief_deps=True to get full issue but compact dependencies.
|
|
~90% smaller than full LinkedIssue.
|
|
"""
|
|
id: str
|
|
title: str
|
|
status: IssueStatus
|
|
priority: int = Field(ge=0, le=4)
|
|
dependency_type: DependencyType | None = None
|
|
|
|
|
|
class OperationResult(BaseModel):
|
|
"""Minimal confirmation for write operations.
|
|
|
|
Default response for create/update/close/reopen when verbose=False.
|
|
~97% smaller than returning full Issue object.
|
|
"""
|
|
id: str
|
|
action: OperationAction
|
|
message: str | None = None
|
|
|
|
|
|
# =============================================================================
|
|
# ORIGINAL MODELS (unchanged for backward compatibility)
|
|
# =============================================================================
|
|
|
|
class IssueBase(BaseModel):
|
|
"""Base issue model with shared fields."""
|
|
|
|
id: str
|
|
title: str
|
|
description: str = ""
|
|
design: str | None = None
|
|
acceptance_criteria: str | None = None
|
|
notes: str | None = None
|
|
external_ref: str | None = None
|
|
status: IssueStatus
|
|
priority: int = Field(ge=0, le=4)
|
|
issue_type: IssueType
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
closed_at: datetime | None = None
|
|
assignee: str | None = None
|
|
labels: list[str] = Field(default_factory=list)
|
|
dependency_count: int = 0
|
|
dependent_count: int = 0
|
|
|
|
@field_validator("priority")
|
|
@classmethod
|
|
def validate_priority(cls, v: int) -> int:
|
|
"""Validate priority is 0-4."""
|
|
if not 0 <= v <= 4:
|
|
raise ValueError("Priority must be between 0 and 4")
|
|
return v
|
|
|
|
|
|
class LinkedIssue(IssueBase):
|
|
"""Issue reference in dependencies/dependents (avoids recursion)."""
|
|
|
|
dependency_type: DependencyType | None = None
|
|
|
|
|
|
class Issue(IssueBase):
|
|
"""Issue model matching bd JSON output."""
|
|
|
|
dependencies: list[LinkedIssue] = Field(default_factory=list)
|
|
dependents: list[LinkedIssue] = Field(default_factory=list)
|
|
|
|
|
|
class Dependency(BaseModel):
|
|
"""Dependency relationship model."""
|
|
|
|
from_id: str
|
|
to_id: str
|
|
dep_type: DependencyType
|
|
|
|
|
|
class CreateIssueParams(BaseModel):
|
|
"""Parameters for creating an issue."""
|
|
|
|
title: str
|
|
description: str = ""
|
|
design: str | None = None
|
|
acceptance: str | None = None
|
|
external_ref: str | None = None
|
|
priority: int = Field(default=2, ge=0, le=4)
|
|
issue_type: IssueType = "task"
|
|
assignee: str | None = None
|
|
labels: list[str] = Field(default_factory=list)
|
|
id: str | None = None
|
|
deps: list[str] = Field(default_factory=list)
|
|
|
|
|
|
class UpdateIssueParams(BaseModel):
|
|
"""Parameters for updating an issue."""
|
|
|
|
issue_id: str
|
|
status: IssueStatus | None = None
|
|
priority: int | None = Field(default=None, ge=0, le=4)
|
|
assignee: str | None = None
|
|
title: str | None = None
|
|
description: str | None = None
|
|
design: str | None = None
|
|
acceptance_criteria: str | None = None
|
|
notes: str | None = None
|
|
external_ref: str | None = None
|
|
|
|
|
|
class CloseIssueParams(BaseModel):
|
|
"""Parameters for closing an issue."""
|
|
|
|
issue_id: str
|
|
reason: str = "Completed"
|
|
|
|
|
|
class ReopenIssueParams(BaseModel):
|
|
"""Parameters for reopening issues."""
|
|
|
|
issue_ids: list[str]
|
|
reason: str | None = None
|
|
|
|
|
|
class AddDependencyParams(BaseModel):
|
|
"""Parameters for adding a dependency."""
|
|
|
|
issue_id: str
|
|
depends_on_id: str
|
|
dep_type: DependencyType = "blocks"
|
|
|
|
|
|
class ReadyWorkParams(BaseModel):
|
|
"""Parameters for querying ready work."""
|
|
|
|
limit: int = Field(default=10, ge=1, le=100)
|
|
priority: int | None = Field(default=None, ge=0, le=4)
|
|
assignee: str | None = None
|
|
labels: list[str] | None = None # AND: must have ALL labels
|
|
labels_any: list[str] | None = None # OR: must have at least one
|
|
unassigned: bool = False # Filter to only unassigned issues
|
|
sort_policy: str | None = None # hybrid, priority, oldest
|
|
|
|
|
|
class ListIssuesParams(BaseModel):
|
|
"""Parameters for listing issues."""
|
|
|
|
status: IssueStatus | None = None
|
|
priority: int | None = Field(default=None, ge=0, le=4)
|
|
issue_type: IssueType | None = None
|
|
assignee: str | None = None
|
|
labels: list[str] | None = None # AND: must have ALL labels
|
|
labels_any: list[str] | None = None # OR: must have at least one
|
|
query: str | None = None # Search in title (case-insensitive)
|
|
unassigned: bool = False # Filter to only unassigned issues
|
|
limit: int = Field(default=20, ge=1, le=100) # Reduced to avoid MCP buffer overflow
|
|
|
|
|
|
class ShowIssueParams(BaseModel):
|
|
"""Parameters for showing issue details."""
|
|
|
|
issue_id: str
|
|
|
|
|
|
class Stats(BaseModel):
|
|
"""Beads task statistics."""
|
|
|
|
total_issues: int
|
|
open_issues: int
|
|
in_progress_issues: int
|
|
closed_issues: int
|
|
blocked_issues: int
|
|
ready_issues: int
|
|
average_lead_time_hours: float
|
|
|
|
|
|
class BlockedIssue(Issue):
|
|
"""Blocked issue with blocking information."""
|
|
|
|
blocked_by_count: int
|
|
blocked_by: list[str]
|
|
|
|
|
|
class InitParams(BaseModel):
|
|
"""Parameters for initializing bd."""
|
|
|
|
prefix: str | None = None
|
|
|
|
|
|
class InitResult(BaseModel):
|
|
"""Result from bd init command."""
|
|
|
|
database: str
|
|
prefix: str
|
|
message: str
|