Merge branch 'v2/main' into v2/fix/2224-union-required-undeclared

This commit is contained in:
Cliff Hall 2026-09-06 17:57:52 -04:00 • committed by GitHub
commit 665e8aa84a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 482 additions and 44 deletions

View file

@ -116,9 +116,12 @@ export class FakeInspectorClient
listResourceTemplates = vi.fn(
async () => this.resourceTemplatePages.shift() ?? { resourceTemplates: [] },
);
listRequestorTasks = vi.fn(
async () => this.taskPages.shift() ?? { tasks: [] },
);
// Typed by its signature rather than inferred, so `mock.calls` carries the
// cursor a test asserts on — the empty-string pagination case (#2220) turns
// on *which* cursor each call received, not just how many there were.
listRequestorTasks = vi.fn<
(cursor?: string) => Promise<ListResult<"tasks", Task>>
>(async () => this.taskPages.shift() ?? { tasks: [] });
listSkills = vi.fn(async () => this.skillPages.shift() ?? { skills: [] });
// `skills/get` echoes a minimal entry; tests that care override the mock.
getSkill = vi.fn(async (uri: string) => ({

View file

@ -3045,7 +3045,13 @@ export class InspectorClient extends InspectorClientEventTarget {
throw new Error("Client is not connected");
}
const result = await this.client.request(
{ method: "tasks/list", params: cursor ? { cursor } : {} },
{
method: "tasks/list",
// `!== undefined`, not truthiness: a cursor is opaque and `""` is a
// legal value a server may hand back. Dropping it asks for page one
// again, so a caller walking pages would loop on the first page.
params: cursor !== undefined ? { cursor } : {},
},
ListTasksResultSchema,
this.getRequestOptions(),
);
@ -5242,7 +5248,10 @@ export class InspectorClient extends InspectorClientEventTarget {
const effectiveMeta = this.mergeMeta(metadata);
const params: ListResourcesRequest["params"] = {
...(effectiveMeta ? { _meta: effectiveMeta } : {}),
...(cursor ? { cursor } : {}),
// `!== undefined`, not truthiness: a cursor is opaque and `""` is a
// legal value a server may hand back. Dropping it asks for page one
// again, so a caller walking pages would loop on the first page.
...(cursor !== undefined ? { cursor } : {}),
};
const response = await this.invokeMcpClient(() =>
this.client!.request(
@ -5416,7 +5425,10 @@ export class InspectorClient extends InspectorClientEventTarget {
const effectiveMeta = this.mergeMeta(metadata);
const params: ListResourceTemplatesRequest["params"] = {
...(effectiveMeta ? { _meta: effectiveMeta } : {}),
...(cursor ? { cursor } : {}),
// `!== undefined`, not truthiness: a cursor is opaque and `""` is a
// legal value a server may hand back. Dropping it asks for page one
// again, so a caller walking pages would loop on the first page.
...(cursor !== undefined ? { cursor } : {}),
};
const response = await this.invokeMcpClient(
() =>
@ -5480,7 +5492,10 @@ export class InspectorClient extends InspectorClientEventTarget {
const effectiveMeta = this.mergeMeta(metadata);
const params: ListPromptsRequest["params"] = {
...(effectiveMeta ? { _meta: effectiveMeta } : {}),
...(cursor ? { cursor } : {}),
// `!== undefined`, not truthiness: a cursor is opaque and `""` is a
// legal value a server may hand back. Dropping it asks for page one
// again, so a caller walking pages would loop on the first page.
...(cursor !== undefined ? { cursor } : {}),
};
const response = await this.invokeMcpClient(() =>
this.client!.request(

View file

@ -181,7 +181,13 @@ export class ManagedRequestorTasksState extends TypedEventTarget<ManagedRequesto
? { ...t, status: "cancelled" as const }
: t,
);
this.tasks = cursor ? [...this.tasks, ...page] : page;
// `=== undefined`, not truthiness, on both the append test and the loop
// condition: a cursor is opaque and `""` is a legal value (#2220). Under
// a truthiness check an empty `nextCursor` ended the walk after page one
// AND made the next page replace the list rather than extend it — the
// user-visible task list silently stopping at four rows against a
// conforming server.
this.tasks = cursor === undefined ? page : [...this.tasks, ...page];
cursor = result.nextCursor;
pageCount++;
if (pageCount >= MAX_PAGES) {
@ -189,7 +195,7 @@ export class ManagedRequestorTasksState extends TypedEventTarget<ManagedRequesto
`Maximum pagination limit (${MAX_PAGES} pages) reached while listing requestor tasks`,
);
}
} while (cursor);
} while (cursor !== undefined);
this.dispatchTypedEvent("tasksChange", this.tasks);
return this.getTasks();
}