Skip to content

Commit 2d61b4b

Browse files
committed
adding README
1 parent eed97c9 commit 2d61b4b

File tree

2 files changed

+357
-0
lines changed

2 files changed

+357
-0
lines changed

README.md

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -635,6 +635,108 @@ const transport = new StdioServerTransport();
635635
await server.connect(transport);
636636
```
637637

638+
### Eliciting User Input
639+
640+
MCP servers can request additional information from users through the elicitation feature. This is useful for interactive workflows where the server needs user input or confirmation:
641+
642+
```typescript
643+
// Server-side: Restaurant booking tool that asks for alternatives
644+
server.tool(
645+
"book-restaurant",
646+
{
647+
restaurant: z.string(),
648+
date: z.string(),
649+
partySize: z.number()
650+
},
651+
async ({ restaurant, date, partySize }) => {
652+
// Check availability
653+
const available = await checkAvailability(restaurant, date, partySize);
654+
655+
if (!available) {
656+
// Ask user if they want to try alternative dates
657+
const result = await server.server.elicitInput({
658+
message: `No tables available at ${restaurant} on ${date}. Would you like to check alternative dates?`,
659+
requestedSchema: {
660+
type: "object",
661+
properties: {
662+
checkAlternatives: {
663+
type: "boolean",
664+
title: "Check alternative dates",
665+
description: "Would you like me to check other dates?"
666+
},
667+
flexibleDates: {
668+
type: "string",
669+
title: "Date flexibility",
670+
description: "How flexible are your dates?",
671+
enum: ["next_day", "same_week", "next_week"],
672+
enumNames: ["Next day", "Same week", "Next week"]
673+
}
674+
},
675+
required: ["checkAlternatives"]
676+
}
677+
});
678+
679+
if (result.action === "accept" && result.content?.checkAlternatives) {
680+
const alternatives = await findAlternatives(
681+
restaurant,
682+
date,
683+
partySize,
684+
result.content.flexibleDates as string
685+
);
686+
return {
687+
content: [{
688+
type: "text",
689+
text: `Found these alternatives: ${alternatives.join(", ")}`
690+
}]
691+
};
692+
}
693+
694+
return {
695+
content: [{
696+
type: "text",
697+
text: "No booking made. Original date not available."
698+
}]
699+
};
700+
}
701+
702+
// Book the table
703+
await makeBooking(restaurant, date, partySize);
704+
return {
705+
content: [{
706+
type: "text",
707+
text: `Booked table for ${partySize} at ${restaurant} on ${date}`
708+
}]
709+
};
710+
}
711+
);
712+
713+
// Client-side: Handle elicitation requests
714+
715+
716+
// This is a placeholder - implement based on your UI framework
717+
async function getInputFromUser(message: string, schema: any): Promise<{
718+
action: "accept" | "decline" | "cancel";
719+
data?: Record<string, any>;
720+
}> {
721+
// This should be implemented depending on the app
722+
throw new Error("getInputFromUser must be implemented for your platform");
723+
}
724+
725+
client.setRequestHandler(ElicitRequestSchema, async (request) => {
726+
const userResponse = await getInputFromUser(
727+
request.params.message,
728+
request.params.requestedSchema
729+
);
730+
731+
return {
732+
action: userResponse.action,
733+
content: userResponse.action === "accept" ? userResponse.data : undefined
734+
};
735+
});
736+
```
737+
738+
**Note**: Elicitation requires client support. Clients must declare the `elicitation` capability during initialization.
739+
638740
### Writing MCP Clients
639741

640742
The SDK provides a high-level client interface:

src/server/mcp.test.ts

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
LoggingMessageNotificationSchema,
1515
Notification,
1616
TextContent,
17+
ElicitRequestSchema,
1718
} from "../types.js";
1819
import { ResourceTemplate } from "./mcp.js";
1920
import { completable } from "./completable.js";
@@ -3457,4 +3458,258 @@ describe("prompt()", () => {
34573458
expect(typeof receivedRequestId === 'string' || typeof receivedRequestId === 'number').toBe(true);
34583459
expect(result.messages[0].content.text).toContain("Received request ID:");
34593460
});
3461+
3462+
/**
3463+
* Test: Restaurant booking example with elicitation from README
3464+
*/
3465+
describe("Restaurant booking elicitation example", () => {
3466+
// Mock restaurant booking functions
3467+
const checkAvailability = jest.fn().mockResolvedValue(false);
3468+
const findAlternatives = jest.fn().mockResolvedValue([]);
3469+
const makeBooking = jest.fn().mockResolvedValue("BOOKING-123");
3470+
3471+
let mcpServer: McpServer;
3472+
let client: Client;
3473+
3474+
beforeEach(() => {
3475+
// Reset mocks
3476+
jest.clearAllMocks();
3477+
3478+
// Create server with restaurant booking tool
3479+
mcpServer = new McpServer({
3480+
name: "restaurant-booking-server",
3481+
version: "1.0.0",
3482+
});
3483+
3484+
// Register the restaurant booking tool from README example
3485+
mcpServer.tool(
3486+
"book-restaurant",
3487+
{
3488+
restaurant: z.string(),
3489+
date: z.string(),
3490+
partySize: z.number()
3491+
},
3492+
async ({ restaurant, date, partySize }) => {
3493+
// Check availability
3494+
const available = await checkAvailability(restaurant, date, partySize);
3495+
3496+
if (!available) {
3497+
// Ask user if they want to try alternative dates
3498+
const result = await mcpServer.server.elicitInput({
3499+
message: `No tables available at ${restaurant} on ${date}. Would you like to check alternative dates?`,
3500+
requestedSchema: {
3501+
type: "object",
3502+
properties: {
3503+
checkAlternatives: {
3504+
type: "boolean",
3505+
title: "Check alternative dates",
3506+
description: "Would you like me to check other dates?"
3507+
},
3508+
flexibleDates: {
3509+
type: "string",
3510+
title: "Date flexibility",
3511+
description: "How flexible are your dates?",
3512+
enum: ["next_day", "same_week", "next_week"],
3513+
enumNames: ["Next day", "Same week", "Next week"]
3514+
}
3515+
},
3516+
required: ["checkAlternatives"]
3517+
}
3518+
});
3519+
3520+
if (result.action === "accept" && result.content?.checkAlternatives) {
3521+
const alternatives = await findAlternatives(
3522+
restaurant,
3523+
date,
3524+
partySize,
3525+
result.content.flexibleDates as string
3526+
);
3527+
return {
3528+
content: [{
3529+
type: "text",
3530+
text: `Found these alternatives: ${alternatives.join(", ")}`
3531+
}]
3532+
};
3533+
}
3534+
3535+
return {
3536+
content: [{
3537+
type: "text",
3538+
text: "No booking made. Original date not available."
3539+
}]
3540+
};
3541+
}
3542+
3543+
// Book the table
3544+
await makeBooking(restaurant, date, partySize);
3545+
return {
3546+
content: [{
3547+
type: "text",
3548+
text: `Booked table for ${partySize} at ${restaurant} on ${date}`
3549+
}]
3550+
};
3551+
}
3552+
);
3553+
3554+
// Create client with elicitation capability
3555+
client = new Client(
3556+
{
3557+
name: "test-client",
3558+
version: "1.0.0",
3559+
},
3560+
{
3561+
capabilities: {
3562+
elicitation: {},
3563+
},
3564+
}
3565+
);
3566+
});
3567+
3568+
test("should successfully book when table is available", async () => {
3569+
// Mock availability check to return true
3570+
checkAvailability.mockResolvedValue(true);
3571+
makeBooking.mockResolvedValue("BOOKING-123");
3572+
3573+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
3574+
3575+
await Promise.all([
3576+
client.connect(clientTransport),
3577+
mcpServer.server.connect(serverTransport),
3578+
]);
3579+
3580+
// Call the tool
3581+
const result = await client.callTool({
3582+
name: "book-restaurant",
3583+
arguments: {
3584+
restaurant: "ABC Restaurant",
3585+
date: "2024-12-25",
3586+
partySize: 2
3587+
}
3588+
});
3589+
3590+
expect(checkAvailability).toHaveBeenCalledWith("ABC Restaurant", "2024-12-25", 2);
3591+
expect(makeBooking).toHaveBeenCalledWith("ABC Restaurant", "2024-12-25", 2);
3592+
expect(result.content).toEqual([{
3593+
type: "text",
3594+
text: "Booked table for 2 at ABC Restaurant on 2024-12-25"
3595+
}]);
3596+
});
3597+
3598+
test("should ask for alternatives when no availability and user accepts", async () => {
3599+
// Mock availability check to return false
3600+
checkAvailability.mockResolvedValue(false);
3601+
findAlternatives.mockResolvedValue(["2024-12-26", "2024-12-27", "2024-12-28"]);
3602+
3603+
// Set up client to accept alternative date checking
3604+
client.setRequestHandler(ElicitRequestSchema, async (request) => {
3605+
expect(request.params.message).toContain("No tables available at ABC Restaurant on 2024-12-25");
3606+
return {
3607+
action: "accept",
3608+
content: {
3609+
checkAlternatives: true,
3610+
flexibleDates: "same_week"
3611+
}
3612+
};
3613+
});
3614+
3615+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
3616+
3617+
await Promise.all([
3618+
client.connect(clientTransport),
3619+
mcpServer.server.connect(serverTransport),
3620+
]);
3621+
3622+
// Call the tool
3623+
const result = await client.callTool({
3624+
name: "book-restaurant",
3625+
arguments: {
3626+
restaurant: "ABC Restaurant",
3627+
date: "2024-12-25",
3628+
partySize: 2
3629+
}
3630+
});
3631+
3632+
expect(checkAvailability).toHaveBeenCalledWith("ABC Restaurant", "2024-12-25", 2);
3633+
expect(findAlternatives).toHaveBeenCalledWith("ABC Restaurant", "2024-12-25", 2, "same_week");
3634+
expect(result.content).toEqual([{
3635+
type: "text",
3636+
text: "Found these alternatives: 2024-12-26, 2024-12-27, 2024-12-28"
3637+
}]);
3638+
});
3639+
3640+
test("should handle user declining to check alternatives", async () => {
3641+
// Mock availability check to return false
3642+
checkAvailability.mockResolvedValue(false);
3643+
3644+
// Set up client to decline alternative date checking
3645+
client.setRequestHandler(ElicitRequestSchema, async () => {
3646+
return {
3647+
action: "accept",
3648+
content: {
3649+
checkAlternatives: false
3650+
}
3651+
};
3652+
});
3653+
3654+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
3655+
3656+
await Promise.all([
3657+
client.connect(clientTransport),
3658+
mcpServer.server.connect(serverTransport),
3659+
]);
3660+
3661+
// Call the tool
3662+
const result = await client.callTool({
3663+
name: "book-restaurant",
3664+
arguments: {
3665+
restaurant: "ABC Restaurant",
3666+
date: "2024-12-25",
3667+
partySize: 2
3668+
}
3669+
});
3670+
3671+
expect(checkAvailability).toHaveBeenCalledWith("ABC Restaurant", "2024-12-25", 2);
3672+
expect(findAlternatives).not.toHaveBeenCalled();
3673+
expect(result.content).toEqual([{
3674+
type: "text",
3675+
text: "No booking made. Original date not available."
3676+
}]);
3677+
});
3678+
3679+
test("should handle user cancelling the elicitation", async () => {
3680+
// Mock availability check to return false
3681+
checkAvailability.mockResolvedValue(false);
3682+
3683+
// Set up client to cancel the elicitation
3684+
client.setRequestHandler(ElicitRequestSchema, async () => {
3685+
return {
3686+
action: "cancel"
3687+
};
3688+
});
3689+
3690+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
3691+
3692+
await Promise.all([
3693+
client.connect(clientTransport),
3694+
mcpServer.server.connect(serverTransport),
3695+
]);
3696+
3697+
// Call the tool
3698+
const result = await client.callTool({
3699+
name: "book-restaurant",
3700+
arguments: {
3701+
restaurant: "ABC Restaurant",
3702+
date: "2024-12-25",
3703+
partySize: 2
3704+
}
3705+
});
3706+
3707+
expect(checkAvailability).toHaveBeenCalledWith("ABC Restaurant", "2024-12-25", 2);
3708+
expect(findAlternatives).not.toHaveBeenCalled();
3709+
expect(result.content).toEqual([{
3710+
type: "text",
3711+
text: "No booking made. Original date not available."
3712+
}]);
3713+
});
3714+
});
34603715
});

0 commit comments

Comments
 (0)