Apply sorting before pagination and expose documented MCP options #116
Description
Work
Implement the #115 sorting contract in authorized service queries before any Take/Skip or cursor limit. Add optional MCP sort parameters with accurate descriptions.
Acceptance
- Large fixtures beyond the existing 300/100 limits yield the correct selected page.
- Existing MCP clients retain documented default behavior.
- No duplicate or missing rows for a stable dataset and equal timestamps.
- Task hierarchy is preserved through a documented hierarchy strategy.
What was built
TaskService.List now reads one lightweight projection of identity plus both timestamps for the whole filtered project, picks and orders the page in Page, then loads only the chosen rows. Roots are ordered by the requested sort and each subtree is appended whole, so the cap counts tasks but never cuts inside a subtree: the last root keeps all of its children. A task whose parent was filtered away counts as a root.
KnowledgeService.List orders in SQL with an Id tie-breaker and gained MemoryListFilter (state, type, scope, visibility, project source). The filters narrow the query before Take(ListLimit); they can only narrow what the tenancy predicate already allowed.
task_list and memory_search take an optional sort. ItemSorts.Parse refuses an unknown value and names the accepted ones. System guide raised to v20.
Verification
ListOrderingTests covers 320 tasks against the 300 cap with a subtask straddling the boundary, 120 memories against the 100 cap, eight tasks sharing one timestamp read twice, and edited old items under each sort. McpSortIsOptionalExplicitAndRejectsUnknownValues covers the tool schema, all three values, the unchanged defaults and the refusal.
Not done
No cursor pagination was added; both lists still return a single capped page. The cap is now a documented parameter (TaskService.List(limit), KnowledgeService.ListLimit) rather than a literal, which is what a future cursor would build on.