Merge pull request #54 from modelcontextprotocol/justin/cancellation

Implement cancellation notifications and handling
This commit is contained in:
Justin Spahr-Summers 2024-11-15 13:39:17 +00:00 • committed by GitHub
commit 9db00579b0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 345 additions and 77 deletions

View file

@ -436,3 +436,58 @@ test("should typecheck", () => {
},
});
});
test("should handle client cancelling a request", async () => {
const server = new Server(
{
name: "test server",
version: "1.0",
},
{
capabilities: {
resources: {},
},
},
);
// Set up server to delay responding to listResources
server.setRequestHandler(
ListResourcesRequestSchema,
async (request, extra) => {
await new Promise((resolve) => setTimeout(resolve, 1000));
return {
resources: [],
};
},
);
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
const client = new Client(
{
name: "test client",
version: "1.0",
},
{
capabilities: {},
},
);
await Promise.all([
client.connect(clientTransport),
server.connect(serverTransport),
]);
// Set up abort controller
const controller = new AbortController();
// Issue request but cancel it immediately
const listResourcesPromise = client.listResources(undefined, {
signal: controller.signal,
});
controller.abort("Cancelled by test");
// Request should be rejected
await expect(listResourcesPromise).rejects.toBe("Cancelled by test");
});

View file

@ -1,7 +1,7 @@
import {
ProgressCallback,
Protocol,
ProtocolOptions,
RequestOptions,
} from "../shared/protocol.js";
import { Transport } from "../shared/transport.js";
import {
@ -244,6 +244,10 @@ export class Client<
// No specific capability required for initialized
break;
case "notifications/cancelled":
// Cancellation notifications are always allowed
break;
case "notifications/progress":
// Progress notifications are always allowed
break;
@ -278,14 +282,11 @@ export class Client<
return this.request({ method: "ping" }, EmptyResultSchema);
}
async complete(
params: CompleteRequest["params"],
onprogress?: ProgressCallback,
) {
async complete(params: CompleteRequest["params"], options?: RequestOptions) {
return this.request(
{ method: "completion/complete", params },
CompleteResultSchema,
onprogress,
options,
);
}
@ -298,56 +299,56 @@ export class Client<
async getPrompt(
params: GetPromptRequest["params"],
onprogress?: ProgressCallback,
options?: RequestOptions,
) {
return this.request(
{ method: "prompts/get", params },
GetPromptResultSchema,
onprogress,
options,
);
}
async listPrompts(
params?: ListPromptsRequest["params"],
onprogress?: ProgressCallback,
options?: RequestOptions,
) {
return this.request(
{ method: "prompts/list", params },
ListPromptsResultSchema,
onprogress,
options,
);
}
async listResources(
params?: ListResourcesRequest["params"],
onprogress?: ProgressCallback,
options?: RequestOptions,
) {
return this.request(
{ method: "resources/list", params },
ListResourcesResultSchema,
onprogress,
options,
);
}
async listResourceTemplates(
params?: ListResourceTemplatesRequest["params"],
onprogress?: ProgressCallback,
options?: RequestOptions,
) {
return this.request(
{ method: "resources/templates/list", params },
ListResourceTemplatesResultSchema,
onprogress,
options,
);
}
async readResource(
params: ReadResourceRequest["params"],
onprogress?: ProgressCallback,
options?: RequestOptions,
) {
return this.request(
{ method: "resources/read", params },
ReadResourceResultSchema,
onprogress,
options,
);
}
@ -370,23 +371,23 @@ export class Client<
resultSchema:
| typeof CallToolResultSchema
| typeof CompatibilityCallToolResultSchema = CallToolResultSchema,
onprogress?: ProgressCallback,
options?: RequestOptions,
) {
return this.request(
{ method: "tools/call", params },
resultSchema,
onprogress,
options,
);
}
async listTools(
params?: ListToolsRequest["params"],
onprogress?: ProgressCallback,
options?: RequestOptions,
) {
return this.request(
{ method: "tools/list", params },
ListToolsResultSchema,
onprogress,
options,
);
}