Skip to content

Unify message-list output shape across listy MCP tools #76

Closed

Description

@d4d0h

Problem

Each listy MCP tool returns messages in a slightly different shape:

Tool Array key Pagination fields `total`?
`search` `results` `next_before_id`, `next_before_sent_at` yes
`history` `results` `next_after_id`, `next_after_sent_at` no
`thread` `messages` ❗ `next_after_id` no
`recent` (same as search — deprecated, being removed)
`digest` `chats[id].messages` ❗ none no

Field sets per message row also diverge: `thread` drops `message_type` and `forwarded_from_name`, `digest` drops `reply_to_message_id` and `message_type`.

An AI agent that learned the shape of one tool trips on another. It also makes pagination resumption awkward — the cursor key names differ, and tools like `chats`/`contacts` don't expose pagination in the MCP schema at all despite supporting it server-side.

Proposal

Canonical message-list response:
```ts
{
messages: MessageRow[], // canonical array key
page: {
has_more: boolean,
next_cursor: string | null, // opaque cursor, hides before_id/after_id/sent_at mechanics
total?: number // optional — expensive count, opt-in via include_total param
}
}
```

Canonical `MessageRow` shape: every tool returns the same fields (with optional `fields: 'minimal' | 'full'` param for payload control). Default to `full`.

Apply to: `search`, `history`, `thread`, `digest` (shape per chat), and any future listy tools.

Also: expose pagination on `chats` and `contacts`/`address_book` where it's silently capped today (1,648 chats → default `limit: 50` truncates without `has_more` signal).

Migration strategy

Option A (clean break): bump MCP server version, breaking change. One-time pain.
Option B (opt-in): accept `format: 'v1' | 'v2'` param per tool, default v1 for now, switch default to v2 after clients update.

Prefer A since the consumer base is small (primarily the issue author's own Claude Code installs). Call out in SPEC / README.

Acceptance

  • All listy MCP tools return `{ messages, page: { has_more, next_cursor, total? } }`
  • Cursor is opaque (AI doesn't assemble `before_id + before_sent_at` manually)
  • `has_more` always present so AI knows when to stop paginating
  • `chats` and `address_book` paginate properly
  • `include_total` param on `search` (default true on first page, false on follow-ups)
  • Server instructions and tool descriptions updated with new shape

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions