instance_messages returns nextCursor and hasMore: true and has no cursor input — every message older than the first page is unreachable over MCP
What happens
instance_messages(instance_id, limit) against production returns a body carrying nextCursor and hasMore: true. There is no way to use either: the tool's whole input schema is
{
token: z.string().optional()…,
instance_id: z.string(),
limit: z.number().int().min(1).max(100).optional(),
}
— workers/mcp/src/instance-tools/observability.ts:21-24
so a caller reads the newest page, is told there is more, and has no argument to ask for it. Raising limit does not help past 100.
Mechanism — the cursor is dropped one layer above the API that already supports it
Verified. The API takes a cursor. workers/api/src/routes/instances-chat.ts:212-219:
const params = new URLSearchParams({ limit: String(limit) });
const before = c.req.query("before");
if (before) params.set("before", before);
with the comment naming the prior incident:
#428: this used to rebuild the DO query string from scratch with only limit, so before never reached the object and "Load older messages" re-served the newest page.
and the DO honours it (agent-do.ts:861-863, resolveCursor(url.searchParams.get("before")), rejecting a bad cursor with a 400 rather than an empty 200).
Verified. The MCP tool builds its URL with limit only — observability.ts:29:
`/v1/instances/${instance_id}/messages?limit=${limit || 50}`
So #428 fixed exactly this shape for the console and the MCP surface was never brought along. The tool is the fixed bug, one layer up.
Verified. The two limit ceilings also disagree: MCP clamps at 100 (:23), the route clamps at 2000 (instances-chat.ts:211). So even the single-page workaround is 20× smaller over MCP than over HTTP.
Why it matters
list_feedback's description (observability.ts:135) points debuggers here explicitly — "the natural next calls are agent_trace(trace_id=…) … and instance_messages for the surrounding conversation" — and a feedback row can be older than 100 messages. So the documented triage sequence for #514 dead-ends on a long conversation. The trace itself prunes at 14 days (lib/events.ts:69-76); the message history does not, which makes the transcript the only record of an older turn, and it is the one MCP cannot page.
What to do
One-line fix, in the tool that already proxies the right route:
- Add
before: z.string().optional().describe("Cursor from a previous call's nextCursor — returns the page OLDER than it.").
- Append
&before=${encodeURIComponent(before)} when present.
- Raise the
limit max to match the route (2000), or state in the description why MCP is stricter.
- Mention in the description that the response carries
nextCursor/hasMore and how to use them — an undocumented output field is how this stayed invisible.
Alternatives considered and rejected
Acceptance criteria
instance_messages accepts a cursor and, given a nextCursor from call N, call N+1 returns strictly older messages.
- An invalid cursor surfaces the route's 400 rather than an empty page (the property
instances-chat.ts:222 explicitly preserves).
- A test drives two consecutive pages over the MCP tool and asserts no overlap and no gap.
Regression risk
Low — additive optional input. The one thing to check is that the cursor is passed through encodeURIComponent: the DO's cursor format is msg:<iso>:<id> (see instances.integration.test.ts:601) and the : characters are safe unencoded but the id is not guaranteed to be.
Related: #428 (the same defect, fixed for the console), #514 (list_feedback → instance_messages is the documented triage path this blocks).
instance_messagesreturnsnextCursorandhasMore: trueand has no cursor input — every message older than the first page is unreachable over MCPWhat happens
instance_messages(instance_id, limit)against production returns a body carryingnextCursorandhasMore: true. There is no way to use either: the tool's whole input schema is—
workers/mcp/src/instance-tools/observability.ts:21-24so a caller reads the newest page, is told there is more, and has no argument to ask for it. Raising
limitdoes not help past 100.Mechanism — the cursor is dropped one layer above the API that already supports it
Verified. The API takes a cursor.
workers/api/src/routes/instances-chat.ts:212-219:with the comment naming the prior incident:
and the DO honours it (
agent-do.ts:861-863,resolveCursor(url.searchParams.get("before")), rejecting a bad cursor with a 400 rather than an empty 200).Verified. The MCP tool builds its URL with
limitonly —observability.ts:29:`/v1/instances/${instance_id}/messages?limit=${limit || 50}`So
#428fixed exactly this shape for the console and the MCP surface was never brought along. The tool is the fixed bug, one layer up.Verified. The two
limitceilings also disagree: MCP clamps at 100 (:23), the route clamps at 2000 (instances-chat.ts:211). So even the single-page workaround is 20× smaller over MCP than over HTTP.Why it matters
list_feedback's description (observability.ts:135) points debuggers here explicitly — "the natural next calls are agent_trace(trace_id=…) … and instance_messages for the surrounding conversation" — and a feedback row can be older than 100 messages. So the documented triage sequence for #514 dead-ends on a long conversation. The trace itself prunes at 14 days (lib/events.ts:69-76); the message history does not, which makes the transcript the only record of an older turn, and it is the one MCP cannot page.What to do
One-line fix, in the tool that already proxies the right route:
before: z.string().optional().describe("Cursor from a previous call's nextCursor — returns the page OLDER than it.").&before=${encodeURIComponent(before)}when present.limitmax to match the route (2000), or state in the description why MCP is stricter.nextCursor/hasMoreand how to use them — an undocumented output field is how this stayed invisible.Alternatives considered and rejected
nextCursor/hasMorefrom the MCP response instead. Cheaper, and wrong: it removes the only signal that history was cut, leaving a caller to conclude a 500-message conversation is 50 messages long. Advertising a capability you lack is bad; silently truncating is worse.instance_messages_pagetool. A second tool for the same read, andtools/listis already 135 entries (The MCP server never publishes tool annotations or output schemas — it already classifies every tool read/write/destructive and tells no host #561/PAGS has no check on what tools/list publishes — adopt the spec schema plus the directory bar, and skip the official conformance suite (it cannot authenticate and grades SDKs) #562).Acceptance criteria
instance_messagesaccepts a cursor and, given anextCursorfrom call N, call N+1 returns strictly older messages.instances-chat.ts:222explicitly preserves).Regression risk
Low — additive optional input. The one thing to check is that the cursor is passed through
encodeURIComponent: the DO's cursor format ismsg:<iso>:<id>(seeinstances.integration.test.ts:601) and the:characters are safe unencoded but the id is not guaranteed to be.Related: #428 (the same defect, fixed for the console), #514 (
list_feedback→instance_messagesis the documented triage path this blocks).