elicitation example
This commit is contained in:
parent
77a284c1ef
commit
04cbbfbb0d
2 changed files with 891 additions and 2 deletions
|
|
@ -10,6 +10,7 @@ import {
|
|||
LATEST_PROTOCOL_VERSION,
|
||||
SUPPORTED_PROTOCOL_VERSIONS,
|
||||
CreateMessageRequestSchema,
|
||||
ElicitRequestSchema,
|
||||
ListPromptsRequestSchema,
|
||||
ListResourcesRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
|
|
@ -267,6 +268,318 @@ test("should respect client capabilities", async () => {
|
|||
await expect(server.listRoots()).rejects.toThrow(/^Client does not support/);
|
||||
});
|
||||
|
||||
test("should respect client elicitation capabilities", async () => {
|
||||
const server = new Server(
|
||||
{
|
||||
name: "test server",
|
||||
version: "1.0",
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
prompts: {},
|
||||
resources: {},
|
||||
tools: {},
|
||||
logging: {},
|
||||
},
|
||||
enforceStrictCapabilities: true,
|
||||
},
|
||||
);
|
||||
|
||||
const client = new Client(
|
||||
{
|
||||
name: "test client",
|
||||
version: "1.0",
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
elicitation: {},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
client.setRequestHandler(ElicitRequestSchema, (params) => ({
|
||||
action: "accept",
|
||||
content: {
|
||||
username: params.params.message.includes("username") ? "test-user" : undefined,
|
||||
confirmed: true,
|
||||
},
|
||||
}));
|
||||
|
||||
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
||||
|
||||
await Promise.all([
|
||||
client.connect(clientTransport),
|
||||
server.connect(serverTransport),
|
||||
]);
|
||||
|
||||
expect(server.getClientCapabilities()).toEqual({ elicitation: {} });
|
||||
|
||||
// This should work because elicitation is supported by the client
|
||||
await expect(
|
||||
server.elicitInput({
|
||||
message: "Please provide your username",
|
||||
requestedSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
username: {
|
||||
type: "string",
|
||||
title: "Username",
|
||||
description: "Your username",
|
||||
},
|
||||
confirmed: {
|
||||
type: "boolean",
|
||||
title: "Confirm",
|
||||
description: "Please confirm",
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
required: ["username"],
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
action: "accept",
|
||||
content: {
|
||||
username: "test-user",
|
||||
confirmed: true,
|
||||
},
|
||||
});
|
||||
|
||||
// This should still throw because sampling is not supported by the client
|
||||
await expect(
|
||||
server.createMessage({
|
||||
messages: [],
|
||||
maxTokens: 10,
|
||||
}),
|
||||
).rejects.toThrow(/^Client does not support/);
|
||||
});
|
||||
|
||||
test("should validate elicitation response against requested schema", async () => {
|
||||
const server = new Server(
|
||||
{
|
||||
name: "test server",
|
||||
version: "1.0",
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
prompts: {},
|
||||
resources: {},
|
||||
tools: {},
|
||||
logging: {},
|
||||
},
|
||||
enforceStrictCapabilities: true,
|
||||
},
|
||||
);
|
||||
|
||||
const client = new Client(
|
||||
{
|
||||
name: "test client",
|
||||
version: "1.0",
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
elicitation: {},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
// Set up client to return valid response
|
||||
client.setRequestHandler(ElicitRequestSchema, (request) => ({
|
||||
action: "accept",
|
||||
content: {
|
||||
name: "John Doe",
|
||||
email: "john@example.com",
|
||||
age: 30,
|
||||
},
|
||||
}));
|
||||
|
||||
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
||||
|
||||
await Promise.all([
|
||||
client.connect(clientTransport),
|
||||
server.connect(serverTransport),
|
||||
]);
|
||||
|
||||
// Test with valid response
|
||||
await expect(
|
||||
server.elicitInput({
|
||||
message: "Please provide your information",
|
||||
requestedSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
name: {
|
||||
type: "string",
|
||||
minLength: 1,
|
||||
},
|
||||
email: {
|
||||
type: "string",
|
||||
minLength: 1,
|
||||
},
|
||||
age: {
|
||||
type: "integer",
|
||||
minimum: 0,
|
||||
maximum: 150,
|
||||
},
|
||||
},
|
||||
required: ["name", "email"],
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
action: "accept",
|
||||
content: {
|
||||
name: "John Doe",
|
||||
email: "john@example.com",
|
||||
age: 30,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("should reject elicitation response with invalid data", async () => {
|
||||
const server = new Server(
|
||||
{
|
||||
name: "test server",
|
||||
version: "1.0",
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
prompts: {},
|
||||
resources: {},
|
||||
tools: {},
|
||||
logging: {},
|
||||
},
|
||||
enforceStrictCapabilities: true,
|
||||
},
|
||||
);
|
||||
|
||||
const client = new Client(
|
||||
{
|
||||
name: "test client",
|
||||
version: "1.0",
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
elicitation: {},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
// Set up client to return invalid response (missing required field, invalid age)
|
||||
client.setRequestHandler(ElicitRequestSchema, (request) => ({
|
||||
action: "accept",
|
||||
content: {
|
||||
email: "", // Invalid - too short
|
||||
age: -5, // Invalid age
|
||||
},
|
||||
}));
|
||||
|
||||
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
||||
|
||||
await Promise.all([
|
||||
client.connect(clientTransport),
|
||||
server.connect(serverTransport),
|
||||
]);
|
||||
|
||||
// Test with invalid response
|
||||
await expect(
|
||||
server.elicitInput({
|
||||
message: "Please provide your information",
|
||||
requestedSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
name: {
|
||||
type: "string",
|
||||
minLength: 1,
|
||||
},
|
||||
email: {
|
||||
type: "string",
|
||||
minLength: 1,
|
||||
},
|
||||
age: {
|
||||
type: "integer",
|
||||
minimum: 0,
|
||||
maximum: 150,
|
||||
},
|
||||
},
|
||||
required: ["name", "email"],
|
||||
},
|
||||
}),
|
||||
).rejects.toThrow(/does not match requested schema/);
|
||||
});
|
||||
|
||||
test("should allow elicitation decline and cancel without validation", async () => {
|
||||
const server = new Server(
|
||||
{
|
||||
name: "test server",
|
||||
version: "1.0",
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
prompts: {},
|
||||
resources: {},
|
||||
tools: {},
|
||||
logging: {},
|
||||
},
|
||||
enforceStrictCapabilities: true,
|
||||
},
|
||||
);
|
||||
|
||||
const client = new Client(
|
||||
{
|
||||
name: "test client",
|
||||
version: "1.0",
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
elicitation: {},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
let requestCount = 0;
|
||||
client.setRequestHandler(ElicitRequestSchema, (request) => {
|
||||
requestCount++;
|
||||
if (requestCount === 1) {
|
||||
return { action: "decline" };
|
||||
} else {
|
||||
return { action: "cancel" };
|
||||
}
|
||||
});
|
||||
|
||||
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
||||
|
||||
await Promise.all([
|
||||
client.connect(clientTransport),
|
||||
server.connect(serverTransport),
|
||||
]);
|
||||
|
||||
const schema = {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
name: { type: "string" as const },
|
||||
},
|
||||
required: ["name"],
|
||||
};
|
||||
|
||||
// Test decline - should not validate
|
||||
await expect(
|
||||
server.elicitInput({
|
||||
message: "Please provide your name",
|
||||
requestedSchema: schema,
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
action: "decline",
|
||||
});
|
||||
|
||||
// Test cancel - should not validate
|
||||
await expect(
|
||||
server.elicitInput({
|
||||
message: "Please provide your name",
|
||||
requestedSchema: schema,
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
action: "cancel",
|
||||
});
|
||||
});
|
||||
|
||||
test("should respect server notification capabilities", async () => {
|
||||
const server = new Server(
|
||||
{
|
||||
|
|
|
|||
|
|
@ -8,6 +8,9 @@ import {
|
|||
ClientCapabilities,
|
||||
CreateMessageRequest,
|
||||
CreateMessageResultSchema,
|
||||
ElicitRequest,
|
||||
ElicitResult,
|
||||
ElicitResultSchema,
|
||||
EmptyResultSchema,
|
||||
Implementation,
|
||||
InitializedNotificationSchema,
|
||||
|
|
@ -18,6 +21,8 @@ import {
|
|||
ListRootsRequest,
|
||||
ListRootsResultSchema,
|
||||
LoggingMessageNotification,
|
||||
McpError,
|
||||
ErrorCode,
|
||||
Notification,
|
||||
Request,
|
||||
ResourceUpdatedNotification,
|
||||
|
|
@ -28,6 +33,7 @@ import {
|
|||
ServerResult,
|
||||
SUPPORTED_PROTOCOL_VERSIONS,
|
||||
} from "../types.js";
|
||||
import { Ajv } from "ajv";
|
||||
|
||||
export type ServerOptions = ProtocolOptions & {
|
||||
/**
|
||||
|
|
@ -129,6 +135,14 @@ export class Server<
|
|||
}
|
||||
break;
|
||||
|
||||
case "elicitation/create":
|
||||
if (!this._clientCapabilities?.elicitation) {
|
||||
throw new Error(
|
||||
`Client does not support elicitation (required for ${method})`,
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
case "roots/list":
|
||||
if (!this._clientCapabilities?.roots) {
|
||||
throw new Error(
|
||||
|
|
@ -294,6 +308,44 @@ export class Server<
|
|||
);
|
||||
}
|
||||
|
||||
async elicitInput(
|
||||
params: ElicitRequest["params"],
|
||||
options?: RequestOptions,
|
||||
): Promise<ElicitResult> {
|
||||
const result = await this.request(
|
||||
{ method: "elicitation/create", params },
|
||||
ElicitResultSchema,
|
||||
options,
|
||||
);
|
||||
|
||||
// Validate the response content against the requested schema if action is "accept"
|
||||
if (result.action === "accept" && result.content) {
|
||||
try {
|
||||
const ajv = new Ajv({ strict: false, validateFormats: true });
|
||||
|
||||
const validate = ajv.compile(params.requestedSchema);
|
||||
const isValid = validate(result.content);
|
||||
|
||||
if (!isValid) {
|
||||
throw new McpError(
|
||||
ErrorCode.InvalidParams,
|
||||
`Elicitation response content does not match requested schema: ${ajv.errorsText(validate.errors)}`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof McpError) {
|
||||
throw error;
|
||||
}
|
||||
throw new McpError(
|
||||
ErrorCode.InternalError,
|
||||
`Error validating elicitation response: ${error}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async listRoots(
|
||||
params?: ListRootsRequest["params"],
|
||||
options?: RequestOptions,
|
||||
|
|
|
|||
Loading…
Reference in a new issue