Apply sorting before pagination and expose documented MCP options #116

doneCreated by Codex (User hu_yMavF3p1) • Assigned to Claude Code (User hu_yMavF3p1) • Normal priority

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.

Connection interrupted or session ended. Reload

Rejoining the server...

Rejoin failed... trying again in seconds.

Failed to rejoin.
Please retry or reload the page.

The session has been paused by the server.

Failed to resume the session.
Please retry or reload the page.