Skip to content

Commit 9db0057

Browse files
Merge pull request #54 from modelcontextprotocol/justin/cancellation
Implement cancellation notifications and handling
2 parents e9efdba + 9882f1c commit 9db0057

File tree

8 files changed

+345
-77
lines changed

8 files changed

+345
-77
lines changed

src/client/index.test.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,3 +436,58 @@ test("should typecheck", () => {
436436
},
437437
});
438438
});
439+
440+
test("should handle client cancelling a request", async () => {
441+
const server = new Server(
442+
{
443+
name: "test server",
444+
version: "1.0",
445+
},
446+
{
447+
capabilities: {
448+
resources: {},
449+
},
450+
},
451+
);
452+
453+
// Set up server to delay responding to listResources
454+
server.setRequestHandler(
455+
ListResourcesRequestSchema,
456+
async (request, extra) => {
457+
await new Promise((resolve) => setTimeout(resolve, 1000));
458+
return {
459+
resources: [],
460+
};
461+
},
462+
);
463+
464+
const [clientTransport, serverTransport] =
465+
InMemoryTransport.createLinkedPair();
466+
467+
const client = new Client(
468+
{
469+
name: "test client",
470+
version: "1.0",
471+
},
472+
{
473+
capabilities: {},
474+
},
475+
);
476+
477+
await Promise.all([
478+
client.connect(clientTransport),
479+
server.connect(serverTransport),
480+
]);
481+
482+
// Set up abort controller
483+
const controller = new AbortController();
484+
485+
// Issue request but cancel it immediately
486+
const listResourcesPromise = client.listResources(undefined, {
487+
signal: controller.signal,
488+
});
489+
controller.abort("Cancelled by test");
490+
491+
// Request should be rejected
492+
await expect(listResourcesPromise).rejects.toBe("Cancelled by test");
493+
});

src/client/index.ts

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {
2-
ProgressCallback,
32
Protocol,
43
ProtocolOptions,
4+
RequestOptions,
55
} from "../shared/protocol.js";
66
import { Transport } from "../shared/transport.js";
77
import {
@@ -244,6 +244,10 @@ export class Client<
244244
// No specific capability required for initialized
245245
break;
246246

247+
case "notifications/cancelled":
248+
// Cancellation notifications are always allowed
249+
break;
250+
247251
case "notifications/progress":
248252
// Progress notifications are always allowed
249253
break;
@@ -278,14 +282,11 @@ export class Client<
278282
return this.request({ method: "ping" }, EmptyResultSchema);
279283
}
280284

281-
async complete(
282-
params: CompleteRequest["params"],
283-
onprogress?: ProgressCallback,
284-
) {
285+
async complete(params: CompleteRequest["params"], options?: RequestOptions) {
285286
return this.request(
286287
{ method: "completion/complete", params },
287288
CompleteResultSchema,
288-
onprogress,
289+
options,
289290
);
290291
}
291292

@@ -298,56 +299,56 @@ export class Client<
298299

299300
async getPrompt(
300301
params: GetPromptRequest["params"],
301-
onprogress?: ProgressCallback,
302+
options?: RequestOptions,
302303
) {
303304
return this.request(
304305
{ method: "prompts/get", params },
305306
GetPromptResultSchema,
306-
onprogress,
307+
options,
307308
);
308309
}
309310

310311
async listPrompts(
311312
params?: ListPromptsRequest["params"],
312-
onprogress?: ProgressCallback,
313+
options?: RequestOptions,
313314
) {
314315
return this.request(
315316
{ method: "prompts/list", params },
316317
ListPromptsResultSchema,
317-
onprogress,
318+
options,
318319
);
319320
}
320321

321322
async listResources(
322323
params?: ListResourcesRequest["params"],
323-
onprogress?: ProgressCallback,
324+
options?: RequestOptions,
324325
) {
325326
return this.request(
326327
{ method: "resources/list", params },
327328
ListResourcesResultSchema,
328-
onprogress,
329+
options,
329330
);
330331
}
331332

332333
async listResourceTemplates(
333334
params?: ListResourceTemplatesRequest["params"],
334-
onprogress?: ProgressCallback,
335+
options?: RequestOptions,
335336
) {
336337
return this.request(
337338
{ method: "resources/templates/list", params },
338339
ListResourceTemplatesResultSchema,
339-
onprogress,
340+
options,
340341
);
341342
}
342343

343344
async readResource(
344345
params: ReadResourceRequest["params"],
345-
onprogress?: ProgressCallback,
346+
options?: RequestOptions,
346347
) {
347348
return this.request(
348349
{ method: "resources/read", params },
349350
ReadResourceResultSchema,
350-
onprogress,
351+
options,
351352
);
352353
}
353354

@@ -370,23 +371,23 @@ export class Client<
370371
resultSchema:
371372
| typeof CallToolResultSchema
372373
| typeof CompatibilityCallToolResultSchema = CallToolResultSchema,
373-
onprogress?: ProgressCallback,
374+
options?: RequestOptions,
374375
) {
375376
return this.request(
376377
{ method: "tools/call", params },
377378
resultSchema,
378-
onprogress,
379+
options,
379380
);
380381
}
381382

382383
async listTools(
383384
params?: ListToolsRequest["params"],
384-
onprogress?: ProgressCallback,
385+
options?: RequestOptions,
385386
) {
386387
return this.request(
387388
{ method: "tools/list", params },
388389
ListToolsResultSchema,
389-
onprogress,
390+
options,
390391
);
391392
}
392393

src/server/index.test.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,3 +407,71 @@ test("should typecheck", () => {
407407
},
408408
);
409409
});
410+
411+
test("should handle server cancelling a request", async () => {
412+
const server = new Server(
413+
{
414+
name: "test server",
415+
version: "1.0",
416+
},
417+
{
418+
capabilities: {
419+
sampling: {},
420+
},
421+
},
422+
);
423+
424+
const client = new Client(
425+
{
426+
name: "test client",
427+
version: "1.0",
428+
},
429+
{
430+
capabilities: {
431+
sampling: {},
432+
},
433+
},
434+
);
435+
436+
// Set up client to delay responding to createMessage
437+
client.setRequestHandler(
438+
CreateMessageRequestSchema,
439+
async (_request, extra) => {
440+
await new Promise((resolve) => setTimeout(resolve, 1000));
441+
return {
442+
model: "test",
443+
role: "assistant",
444+
content: {
445+
type: "text",
446+
text: "Test response",
447+
},
448+
};
449+
},
450+
);
451+
452+
const [clientTransport, serverTransport] =
453+
InMemoryTransport.createLinkedPair();
454+
455+
await Promise.all([
456+
client.connect(clientTransport),
457+
server.connect(serverTransport),
458+
]);
459+
460+
// Set up abort controller
461+
const controller = new AbortController();
462+
463+
// Issue request but cancel it immediately
464+
const createMessagePromise = server.createMessage(
465+
{
466+
messages: [],
467+
maxTokens: 10,
468+
},
469+
{
470+
signal: controller.signal,
471+
},
472+
);
473+
controller.abort("Cancelled by test");
474+
475+
// Request should be rejected
476+
await expect(createMessagePromise).rejects.toBe("Cancelled by test");
477+
});

src/server/index.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {
2-
ProgressCallback,
32
Protocol,
43
ProtocolOptions,
4+
RequestOptions,
55
} from "../shared/protocol.js";
66
import {
77
ClientCapabilities,
@@ -157,6 +157,10 @@ export class Server<
157157
}
158158
break;
159159

160+
case "notifications/cancelled":
161+
// Cancellation notifications are always allowed
162+
break;
163+
160164
case "notifications/progress":
161165
// Progress notifications are always allowed
162166
break;
@@ -257,23 +261,23 @@ export class Server<
257261

258262
async createMessage(
259263
params: CreateMessageRequest["params"],
260-
onprogress?: ProgressCallback,
264+
options?: RequestOptions,
261265
) {
262266
return this.request(
263267
{ method: "sampling/createMessage", params },
264268
CreateMessageResultSchema,
265-
onprogress,
269+
options,
266270
);
267271
}
268272

269273
async listRoots(
270274
params?: ListRootsRequest["params"],
271-
onprogress?: ProgressCallback,
275+
options?: RequestOptions,
272276
) {
273277
return this.request(
274278
{ method: "roots/list", params },
275279
ListRootsResultSchema,
276-
onprogress,
280+
options,
277281
);
278282
}
279283

0 commit comments

Comments
 (0)