adding README

This commit is contained in:
ihrpr 2025-06-10 15:52:20 +01:00
commit 2d61b4b8fc
No known key found for this signature in database
2 changed files with 357 additions and 0 deletions

102
README.md
View file

@ -635,6 +635,108 @@ const transport = new StdioServerTransport();
await server.connect(transport);
```
### Eliciting User Input
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:
```typescript
// Server-side: Restaurant booking tool that asks for alternatives
server.tool(
"book-restaurant",
{
restaurant: z.string(),
date: z.string(),
partySize: z.number()
},
async ({ restaurant, date, partySize }) => {
// Check availability
const available = await checkAvailability(restaurant, date, partySize);
if (!available) {
// Ask user if they want to try alternative dates
const result = await server.server.elicitInput({
message: `No tables available at ${restaurant} on ${date}. Would you like to check alternative dates?`,
requestedSchema: {
type: "object",
properties: {
checkAlternatives: {
type: "boolean",
title: "Check alternative dates",
description: "Would you like me to check other dates?"
},
flexibleDates: {
type: "string",
title: "Date flexibility",
description: "How flexible are your dates?",
enum: ["next_day", "same_week", "next_week"],
enumNames: ["Next day", "Same week", "Next week"]
}
},
required: ["checkAlternatives"]
}
});
if (result.action === "accept" && result.content?.checkAlternatives) {
const alternatives = await findAlternatives(
restaurant,
date,
partySize,
result.content.flexibleDates as string
);
return {
content: [{
type: "text",
text: `Found these alternatives: ${alternatives.join(", ")}`
}]
};
}
return {
content: [{
type: "text",
text: "No booking made. Original date not available."
}]
};
}
// Book the table
await makeBooking(restaurant, date, partySize);
return {
content: [{
type: "text",
text: `Booked table for ${partySize} at ${restaurant} on ${date}`
}]
};
}
);
// Client-side: Handle elicitation requests
// This is a placeholder - implement based on your UI framework
async function getInputFromUser(message: string, schema: any): Promise<{
action: "accept" | "decline" | "cancel";
data?: Record<string, any>;
}> {
// This should be implemented depending on the app
throw new Error("getInputFromUser must be implemented for your platform");
}
client.setRequestHandler(ElicitRequestSchema, async (request) => {
const userResponse = await getInputFromUser(
request.params.message,
request.params.requestedSchema
);
return {
action: userResponse.action,
content: userResponse.action === "accept" ? userResponse.data : undefined
};
});
```
**Note**: Elicitation requires client support. Clients must declare the `elicitation` capability during initialization.
### Writing MCP Clients
The SDK provides a high-level client interface:

View file

@ -14,6 +14,7 @@ import {
LoggingMessageNotificationSchema,
Notification,
TextContent,
ElicitRequestSchema,
} from "../types.js";
import { ResourceTemplate } from "./mcp.js";
import { completable } from "./completable.js";
@ -3457,4 +3458,258 @@ describe("prompt()", () => {
expect(typeof receivedRequestId === 'string' || typeof receivedRequestId === 'number').toBe(true);
expect(result.messages[0].content.text).toContain("Received request ID:");
});
/**
* Test: Restaurant booking example with elicitation from README
*/
describe("Restaurant booking elicitation example", () => {
// Mock restaurant booking functions
const checkAvailability = jest.fn().mockResolvedValue(false);
const findAlternatives = jest.fn().mockResolvedValue([]);
const makeBooking = jest.fn().mockResolvedValue("BOOKING-123");
let mcpServer: McpServer;
let client: Client;
beforeEach(() => {
// Reset mocks
jest.clearAllMocks();
// Create server with restaurant booking tool
mcpServer = new McpServer({
name: "restaurant-booking-server",
version: "1.0.0",
});
// Register the restaurant booking tool from README example
mcpServer.tool(
"book-restaurant",
{
restaurant: z.string(),
date: z.string(),
partySize: z.number()
},
async ({ restaurant, date, partySize }) => {
// Check availability
const available = await checkAvailability(restaurant, date, partySize);
if (!available) {
// Ask user if they want to try alternative dates
const result = await mcpServer.server.elicitInput({
message: `No tables available at ${restaurant} on ${date}. Would you like to check alternative dates?`,
requestedSchema: {
type: "object",
properties: {
checkAlternatives: {
type: "boolean",
title: "Check alternative dates",
description: "Would you like me to check other dates?"
},
flexibleDates: {
type: "string",
title: "Date flexibility",
description: "How flexible are your dates?",
enum: ["next_day", "same_week", "next_week"],
enumNames: ["Next day", "Same week", "Next week"]
}
},
required: ["checkAlternatives"]
}
});
if (result.action === "accept" && result.content?.checkAlternatives) {
const alternatives = await findAlternatives(
restaurant,
date,
partySize,
result.content.flexibleDates as string
);
return {
content: [{
type: "text",
text: `Found these alternatives: ${alternatives.join(", ")}`
}]
};
}
return {
content: [{
type: "text",
text: "No booking made. Original date not available."
}]
};
}
// Book the table
await makeBooking(restaurant, date, partySize);
return {
content: [{
type: "text",
text: `Booked table for ${partySize} at ${restaurant} on ${date}`
}]
};
}
);
// Create client with elicitation capability
client = new Client(
{
name: "test-client",
version: "1.0.0",
},
{
capabilities: {
elicitation: {},
},
}
);
});
test("should successfully book when table is available", async () => {
// Mock availability check to return true
checkAvailability.mockResolvedValue(true);
makeBooking.mockResolvedValue("BOOKING-123");
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
await Promise.all([
client.connect(clientTransport),
mcpServer.server.connect(serverTransport),
]);
// Call the tool
const result = await client.callTool({
name: "book-restaurant",
arguments: {
restaurant: "ABC Restaurant",
date: "2024-12-25",
partySize: 2
}
});
expect(checkAvailability).toHaveBeenCalledWith("ABC Restaurant", "2024-12-25", 2);
expect(makeBooking).toHaveBeenCalledWith("ABC Restaurant", "2024-12-25", 2);
expect(result.content).toEqual([{
type: "text",
text: "Booked table for 2 at ABC Restaurant on 2024-12-25"
}]);
});
test("should ask for alternatives when no availability and user accepts", async () => {
// Mock availability check to return false
checkAvailability.mockResolvedValue(false);
findAlternatives.mockResolvedValue(["2024-12-26", "2024-12-27", "2024-12-28"]);
// Set up client to accept alternative date checking
client.setRequestHandler(ElicitRequestSchema, async (request) => {
expect(request.params.message).toContain("No tables available at ABC Restaurant on 2024-12-25");
return {
action: "accept",
content: {
checkAlternatives: true,
flexibleDates: "same_week"
}
};
});
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
await Promise.all([
client.connect(clientTransport),
mcpServer.server.connect(serverTransport),
]);
// Call the tool
const result = await client.callTool({
name: "book-restaurant",
arguments: {
restaurant: "ABC Restaurant",
date: "2024-12-25",
partySize: 2
}
});
expect(checkAvailability).toHaveBeenCalledWith("ABC Restaurant", "2024-12-25", 2);
expect(findAlternatives).toHaveBeenCalledWith("ABC Restaurant", "2024-12-25", 2, "same_week");
expect(result.content).toEqual([{
type: "text",
text: "Found these alternatives: 2024-12-26, 2024-12-27, 2024-12-28"
}]);
});
test("should handle user declining to check alternatives", async () => {
// Mock availability check to return false
checkAvailability.mockResolvedValue(false);
// Set up client to decline alternative date checking
client.setRequestHandler(ElicitRequestSchema, async () => {
return {
action: "accept",
content: {
checkAlternatives: false
}
};
});
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
await Promise.all([
client.connect(clientTransport),
mcpServer.server.connect(serverTransport),
]);
// Call the tool
const result = await client.callTool({
name: "book-restaurant",
arguments: {
restaurant: "ABC Restaurant",
date: "2024-12-25",
partySize: 2
}
});
expect(checkAvailability).toHaveBeenCalledWith("ABC Restaurant", "2024-12-25", 2);
expect(findAlternatives).not.toHaveBeenCalled();
expect(result.content).toEqual([{
type: "text",
text: "No booking made. Original date not available."
}]);
});
test("should handle user cancelling the elicitation", async () => {
// Mock availability check to return false
checkAvailability.mockResolvedValue(false);
// Set up client to cancel the elicitation
client.setRequestHandler(ElicitRequestSchema, async () => {
return {
action: "cancel"
};
});
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
await Promise.all([
client.connect(clientTransport),
mcpServer.server.connect(serverTransport),
]);
// Call the tool
const result = await client.callTool({
name: "book-restaurant",
arguments: {
restaurant: "ABC Restaurant",
date: "2024-12-25",
partySize: 2
}
});
expect(checkAvailability).toHaveBeenCalledWith("ABC Restaurant", "2024-12-25", 2);
expect(findAlternatives).not.toHaveBeenCalled();
expect(result.content).toEqual([{
type: "text",
text: "No booking made. Original date not available."
}]);
});
});
});