Models — DTO vs Domain vs MCP I/O
This project separates models by concerns to keep API transport, domain behavior, and MCP contracts decoupled.
Layers
- DTO (Data Transfer Objects): mirror ClickUp API payloads for serialization/deserialization.
- Location: clickup_mcp/models/dto/
- Domain models: rich entities used internally (behavior, invariants, identity-only references).
- Location: clickup_mcp/models/domain/
- MCP I/O models: minimal, stable shapes exposed by MCP tools.
- Location:
- I (Input) clickup_mcp/mcp_server/models/inputs/
- O (Output) clickup_mcp/mcp_server/models/outputs/
- Location:
Example: Workspaces (Teams)
- Domain:
ClickUpTeam(clickup_mcp/models/domain/team.py)- Backward-compatible
idalias →team_id.
- Backward-compatible
- MCP output:
WorkspaceListItem,WorkspaceListResult- Location: clickup_mcp/mcp_server/models/outputs/workspace.py
- Tool:
get_authorized_teams()returnsWorkspaceListResult(decorator emitsToolResponse[WorkspaceListResult]).
Projection in tool
items = [WorkspaceListItem(team_id=str(t.team_id or t.id or ""), name=t.name or "") for t in teams]
return WorkspaceListResult(items=items)
Why the split?
- Resilience to upstream changes (DTOs may drift; MCP I/O remains stable).
- Testability of domain behaviors independent from transport.
- Least surprise for tool consumers (small, documented shapes).
Rationale & JSON-Schema
MCP I/O models are the public contract. We generate JSON-Schema from these Pydantic v2 models to drive client typing and contract tests, while DTOs can evolve with upstream without breaking consumers.
Conversion pipeline
Code links
- clickup_mcp/models/dto/
- clickup_mcp/models/domain/
- clickup_mcp/mcp_server/models/inputs/
- clickup_mcp/mcp_server/models/outputs/
Authoring guidance
- Keep DTOs thin: mirror upstream fields; avoid behavior.
- Enforce invariants in Domain; use identity-only references to avoid heavy graphs.
- Trim I/O outputs to what agents need; prefer stable names and small shapes.
Standardized mapping across resources
We consistently apply the pipeline above in all MCP handlers. Each handler:
- Converts inbound DTOs to Domain using a mapper:
XMapper.to_domain(dto) - Projects Domain to MCP Output using the same mapper:
XMapper.to_*_output(domain)
Task
- Mapper:
clickup_mcp/models/mapping/task_mapper.py- DTO → Domain:
TaskMapper.to_domain(resp: TaskResp) -> ClickUpTask - Domain → Output:
TaskMapper.to_task_result_output(),TaskMapper.to_task_list_item_output() - Shared parsing:
clickup_mcp/models/mapping/priority.py(parse_priority_obj,normalize_priority_input)
- DTO → Domain:
- Handler:
clickup_mcp/mcp_server/task.py_taskresp_to_result()and_taskresp_to_list_item()now perform DTO → Domain → Output
List
- Mapper:
clickup_mcp/models/mapping/list_mapper.py- DTO → Domain:
ListMapper.to_domain(resp: ListResp) -> ClickUpList - Domain → Output:
ListMapper.to_list_result_output(),ListMapper.to_list_list_item_output()
- DTO → Domain:
- Handler:
clickup_mcp/mcp_server/list.pylist_create/get/update/list_*now call mapper output functions
Folder
- Mapper:
clickup_mcp/models/mapping/folder_mapper.py- DTO → Domain:
FolderMapper.to_domain(resp: FolderResp) -> ClickUpFolder - Domain → Output:
FolderMapper.to_folder_result_output(),FolderMapper.to_folder_list_item_output()
- DTO → Domain:
- Handler:
clickup_mcp/mcp_server/folder.pyfolder_create/get/update/list_in_spacenow call mapper output functions
Space
- Mapper:
clickup_mcp/models/mapping/space_mapper.py- DTO → Domain:
SpaceMapper.to_domain(resp: SpaceResp) -> ClickUpSpace - Domain → Output:
SpaceMapper.to_space_result_output(),SpaceMapper.to_space_list_item_output()
- DTO → Domain:
- Handler:
clickup_mcp/mcp_server/space.pyspace_get/list/create/updatenow call mapper output functions
Workspace (Teams)
- Handlers
workspace.listandget_authorized_teamsproject minimal output directly from the Team domain models returned by the client.- When upstream returns DTOs, introduce a
TeamMapperto maintain the same pattern.
- When upstream returns DTOs, introduce a
Rationale for centralizing mapping
- Avoids duplicating projection logic across handlers
- Keeps providers’ quirks (e.g., priority id formats) inside mappers
- Makes unit testing easier: test mappers once, keep handlers thin
Anti-patterns
- Leaking raw upstream payloads into I/O outputs.
- Importing HTTP client concerns into Domain models.
- Hiding business rules in DTOs or I/O layers.