Demonstrate registration of tools conditioned upon client capability support. Also, obviated need for clientConnected callback to pass sessionId because we defer initial fetching of roots happens when you run the get-roots-list tool.

* In how-it-works.md,
  - added a section on conditional tool registration

* In server/index.ts
  - import registerConditionalTools
  - in an oninitialized handler for the server, call registerConditionalTools
  - removed clientConnected from ServerFactoryResponse and all mentions in docs

* In tools/index.ts
  - export a registerConditionalTools function
  - refactor/move calls to registerGetRootsListTool, registerTriggerElicitationRequestTool, and registerTriggerSamplingRequestTool out of registerTools and into registerConditionalTools

* In server/roots.ts
  - only act if client supports roots
  - remove setInterval from call to requestRoots. It isn't happening during the initialze handshake anymore, so it doesn't interfere with that process if called immediaately

* In get-roots-list.ts, trigger-elicitation-request.ts, and trigger-sampling-request.ts,
  - only register tool if client supports capability

* Throughout the rest of the files, removing all references to `clientConnected`
This commit is contained in:
cliffhall
2025-12-15 17:51:30 -05:00
parent ebac6314cf
commit 1b8f376b90
13 changed files with 324 additions and 324 deletions

View File

@@ -13,6 +13,8 @@ const config = {
/**
* Registers the 'trigger-elicitation-request' tool.
*
* If the client does not support the elicitation capability, the tool is not registered.
*
* The registered tool sends an elicitation request for the user to provide information
* based on a pre-defined schema of fields including text inputs, booleans, numbers,
* email, dates, enums of various types, etc. It uses validation and handles multiple
@@ -27,188 +29,199 @@ const config = {
* @param {McpServer} server - TThe McpServer instance where the tool will be registered.
*/
export const registerTriggerElicitationRequestTool = (server: McpServer) => {
server.registerTool(
name,
config,
async (args, extra): Promise<CallToolResult> => {
const elicitationResult = await extra.sendRequest(
{
method: "elicitation/create",
params: {
message: "Please provide inputs for the following fields:",
requestedSchema: {
type: "object",
properties: {
name: {
title: "String",
type: "string",
description: "Your full, legal name",
},
check: {
title: "Boolean",
type: "boolean",
description: "Agree to the terms and conditions",
},
firstLine: {
title: "String with default",
type: "string",
description: "Favorite first line of a story",
default: "It was a dark and stormy night.",
},
email: {
title: "String with email format",
type: "string",
format: "email",
description:
"Your email address (will be verified, and never shared with anyone else)",
},
homepage: {
type: "string",
format: "uri",
title: "String with uri format",
description: "Portfolio / personal website",
},
birthdate: {
title: "String with date format",
type: "string",
format: "date",
description: "Your date of birth",
},
integer: {
title: "Integer",
type: "integer",
description:
"Your favorite integer (do not give us your phone number, pin, or other sensitive info)",
minimum: 1,
maximum: 100,
default: 42,
},
number: {
title: "Number in range 1-1000",
type: "number",
description: "Favorite number (there are no wrong answers)",
minimum: 0,
maximum: 1000,
default: 3.14,
},
untitledSingleSelectEnum: {
type: "string",
title: "Untitled Single Select Enum",
description: "Choose your favorite friend",
enum: [
"Monica",
"Rachel",
"Joey",
"Chandler",
"Ross",
"Phoebe",
],
default: "Monica",
},
untitledMultipleSelectEnum: {
type: "array",
title: "Untitled Multiple Select Enum",
description: "Choose your favorite instruments",
minItems: 1,
maxItems: 3,
items: {
// Does the client support elicitation?
const clientCapabilities = server.server.getClientCapabilities() || {};
const clientSupportsElicitation: boolean =
clientCapabilities.elicitation !== undefined;
// If so, register tool
if (clientSupportsElicitation) {
server.registerTool(
name,
config,
async (args, extra): Promise<CallToolResult> => {
const elicitationResult = await extra.sendRequest(
{
method: "elicitation/create",
params: {
message: "Please provide inputs for the following fields:",
requestedSchema: {
type: "object",
properties: {
name: {
title: "String",
type: "string",
enum: ["Guitar", "Piano", "Violin", "Drums", "Bass"],
description: "Your full, legal name",
},
default: ["Guitar"],
},
titledSingleSelectEnum: {
type: "string",
title: "Titled Single Select Enum",
description: "Choose your favorite hero",
oneOf: [
{ const: "hero-1", title: "Superman" },
{ const: "hero-2", title: "Green Lantern" },
{ const: "hero-3", title: "Wonder Woman" },
],
default: "hero-1",
},
titledMultipleSelectEnum: {
type: "array",
title: "Titled Multiple Select Enum",
description: "Choose your favorite types of fish",
minItems: 1,
maxItems: 3,
items: {
anyOf: [
{ const: "fish-1", title: "Tuna" },
{ const: "fish-2", title: "Salmon" },
{ const: "fish-3", title: "Trout" },
check: {
title: "Boolean",
type: "boolean",
description: "Agree to the terms and conditions",
},
firstLine: {
title: "String with default",
type: "string",
description: "Favorite first line of a story",
default: "It was a dark and stormy night.",
},
email: {
title: "String with email format",
type: "string",
format: "email",
description:
"Your email address (will be verified, and never shared with anyone else)",
},
homepage: {
type: "string",
format: "uri",
title: "String with uri format",
description: "Portfolio / personal website",
},
birthdate: {
title: "String with date format",
type: "string",
format: "date",
description: "Your date of birth",
},
integer: {
title: "Integer",
type: "integer",
description:
"Your favorite integer (do not give us your phone number, pin, or other sensitive info)",
minimum: 1,
maximum: 100,
default: 42,
},
number: {
title: "Number in range 1-1000",
type: "number",
description: "Favorite number (there are no wrong answers)",
minimum: 0,
maximum: 1000,
default: 3.14,
},
untitledSingleSelectEnum: {
type: "string",
title: "Untitled Single Select Enum",
description: "Choose your favorite friend",
enum: [
"Monica",
"Rachel",
"Joey",
"Chandler",
"Ross",
"Phoebe",
],
default: "Monica",
},
untitledMultipleSelectEnum: {
type: "array",
title: "Untitled Multiple Select Enum",
description: "Choose your favorite instruments",
minItems: 1,
maxItems: 3,
items: {
type: "string",
enum: ["Guitar", "Piano", "Violin", "Drums", "Bass"],
},
default: ["Guitar"],
},
titledSingleSelectEnum: {
type: "string",
title: "Titled Single Select Enum",
description: "Choose your favorite hero",
oneOf: [
{const: "hero-1", title: "Superman"},
{const: "hero-2", title: "Green Lantern"},
{const: "hero-3", title: "Wonder Woman"},
],
default: "hero-1",
},
titledMultipleSelectEnum: {
type: "array",
title: "Titled Multiple Select Enum",
description: "Choose your favorite types of fish",
minItems: 1,
maxItems: 3,
items: {
anyOf: [
{const: "fish-1", title: "Tuna"},
{const: "fish-2", title: "Salmon"},
{const: "fish-3", title: "Trout"},
],
},
default: ["fish-1"],
},
legacyTitledEnum: {
type: "string",
title: "Legacy Titled Single Select Enum",
description: "Choose your favorite type of pet",
enum: ["pet-1", "pet-2", "pet-3", "pet-4", "pet-5"],
enumNames: ["Cats", "Dogs", "Birds", "Fish", "Reptiles"],
default: "pet-1",
},
default: ["fish-1"],
},
legacyTitledEnum: {
type: "string",
title: "Legacy Titled Single Select Enum",
description: "Choose your favorite type of pet",
enum: ["pet-1", "pet-2", "pet-3", "pet-4", "pet-5"],
enumNames: ["Cats", "Dogs", "Birds", "Fish", "Reptiles"],
default: "pet-1",
},
required: ["name"],
},
required: ["name"],
},
},
},
ElicitResultSchema,
{ timeout: 10 * 60 * 1000 /* 10 minutes */ }
);
ElicitResultSchema,
{timeout: 10 * 60 * 1000 /* 10 minutes */}
);
// Handle different response actions
const content: CallToolResult["content"] = [];
// Handle different response actions
const content: CallToolResult["content"] = [];
if (elicitationResult.action === "accept" && elicitationResult.content) {
if (
elicitationResult.action === "accept" &&
elicitationResult.content
) {
content.push({
type: "text",
text: `✅ User provided the requested information!`,
});
// Only access elicitationResult.content when action is accept
const userData = elicitationResult.content;
const lines = [];
if (userData.name) lines.push(`- Name: ${userData.name}`);
if (userData.check !== undefined)
lines.push(`- Agreed to terms: ${userData.check}`);
if (userData.color) lines.push(`- Favorite Color: ${userData.color}`);
if (userData.email) lines.push(`- Email: ${userData.email}`);
if (userData.homepage) lines.push(`- Homepage: ${userData.homepage}`);
if (userData.birthdate)
lines.push(`- Birthdate: ${userData.birthdate}`);
if (userData.integer !== undefined)
lines.push(`- Favorite Integer: ${userData.integer}`);
if (userData.number !== undefined)
lines.push(`- Favorite Number: ${userData.number}`);
if (userData.petType) lines.push(`- Pet Type: ${userData.petType}`);
content.push({
type: "text",
text: `User inputs:\n${lines.join("\n")}`,
});
} else if (elicitationResult.action === "decline") {
content.push({
type: "text",
text: `❌ User declined to provide the requested information.`,
});
} else if (elicitationResult.action === "cancel") {
content.push({
type: "text",
text: `⚠️ User cancelled the elicitation dialog.`,
});
}
// Include raw result for debugging
content.push({
type: "text",
text: `✅ User provided the requested information!`,
text: `\nRaw result: ${JSON.stringify(elicitationResult, null, 2)}`,
});
// Only access elicitationResult.content when action is accept
const userData = elicitationResult.content;
const lines = [];
if (userData.name) lines.push(`- Name: ${userData.name}`);
if (userData.check !== undefined)
lines.push(`- Agreed to terms: ${userData.check}`);
if (userData.color) lines.push(`- Favorite Color: ${userData.color}`);
if (userData.email) lines.push(`- Email: ${userData.email}`);
if (userData.homepage) lines.push(`- Homepage: ${userData.homepage}`);
if (userData.birthdate)
lines.push(`- Birthdate: ${userData.birthdate}`);
if (userData.integer !== undefined)
lines.push(`- Favorite Integer: ${userData.integer}`);
if (userData.number !== undefined)
lines.push(`- Favorite Number: ${userData.number}`);
if (userData.petType) lines.push(`- Pet Type: ${userData.petType}`);
content.push({
type: "text",
text: `User inputs:\n${lines.join("\n")}`,
});
} else if (elicitationResult.action === "decline") {
content.push({
type: "text",
text: `❌ User declined to provide the requested information.`,
});
} else if (elicitationResult.action === "cancel") {
content.push({
type: "text",
text: `⚠️ User cancelled the elicitation dialog.`,
});
return {content};
}
// Include raw result for debugging
content.push({
type: "text",
text: `\nRaw result: ${JSON.stringify(elicitationResult, null, 2)}`,
});
return { content };
}
);
);
}
};