mirror of
https://github.com/modelcontextprotocol/servers.git
synced 2026-04-20 00:53:24 +02:00
[WIP] Refactor everything server to be more modular and use recommended APIs.
Adding Trigger Elicitation Request and Get Roots List tools
* Updated architecture.md
* Added roots.ts
- tracks roots by sessionId
- setRootsListChangedHandler
- listens for roots changed notification from the client
- updates the roots map by sessionId
- sends log notification or error to the client
* In server/index.ts
- import setRootsListChangedHandler
- in clientConnected callback
- call setRootsListChangedHandler passing server and sessionId
* In sse.ts, stdio.ts, and streamableHttp.ts
- receive clientConnected from server factory
- call clientConnected when server is connected to transport
* Added get-roots-list.ts
- registerGetRootsListTool
- Registers the 'get-roots-list' tool with the given MCP server.
* Added trigger-elicitation-request.ts
- registerTriggerElicitationRequestTool
- registered tool sends an elicitation request that exercises all supported field types
* In tools/index.ts
- imports registerTriggerElicitationRequestTool and registerGetRootsListTool
- in registerTools
- call registerTriggerElicitationRequestTool and registerGetRootsListTool, passing server
This commit is contained in:
@@ -10,6 +10,7 @@ import { registerTools } from "../tools/index.js";
|
|||||||
import { registerResources } from "../resources/index.js";
|
import { registerResources } from "../resources/index.js";
|
||||||
import { registerPrompts } from "../prompts/index.js";
|
import { registerPrompts } from "../prompts/index.js";
|
||||||
import { stopSimulatedLogging } from "./logging.js";
|
import { stopSimulatedLogging } from "./logging.js";
|
||||||
|
import { setRootsListChangedHandler } from "./roots.js";
|
||||||
|
|
||||||
// Everything Server factory
|
// Everything Server factory
|
||||||
export const createServer = () => {
|
export const createServer = () => {
|
||||||
@@ -48,9 +49,13 @@ export const createServer = () => {
|
|||||||
// Set resource subscription handlers
|
// Set resource subscription handlers
|
||||||
setSubscriptionHandlers(server);
|
setSubscriptionHandlers(server);
|
||||||
|
|
||||||
// Return server instance and cleanup function
|
// Return server instance, client connection handler, and cleanup function
|
||||||
return {
|
return {
|
||||||
server,
|
server,
|
||||||
|
clientConnected: (sessionId?: string) => {
|
||||||
|
// Set the roots list changed handler
|
||||||
|
setRootsListChangedHandler(server, sessionId);
|
||||||
|
},
|
||||||
cleanup: (sessionId?: string) => {
|
cleanup: (sessionId?: string) => {
|
||||||
// Stop any simulated logging or resource updates that may have been initiated.
|
// Stop any simulated logging or resource updates that may have been initiated.
|
||||||
stopSimulatedLogging(sessionId);
|
stopSimulatedLogging(sessionId);
|
||||||
|
|||||||
63
src/everything/server/roots.ts
Normal file
63
src/everything/server/roots.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||||
|
import {
|
||||||
|
Root,
|
||||||
|
RootsListChangedNotificationSchema,
|
||||||
|
} from "@modelcontextprotocol/sdk/types.js";
|
||||||
|
|
||||||
|
// Track roots by session id
|
||||||
|
const roots: Map<string | undefined, Root[]> = new Map<
|
||||||
|
string | undefined,
|
||||||
|
Root[]
|
||||||
|
>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a handler for the "RootsListChanged" notification from the client.
|
||||||
|
*
|
||||||
|
* This handler updates the local roots list when notified and logs relevant
|
||||||
|
* acknowledgement or error.
|
||||||
|
*
|
||||||
|
* @param {McpServer} mcpServer - The instance of the McpServer managing server communication.
|
||||||
|
* @param {string | undefined} sessionId - An optional session ID used for logging purposes.
|
||||||
|
*/
|
||||||
|
export const setRootsListChangedHandler = (
|
||||||
|
mcpServer: McpServer,
|
||||||
|
sessionId?: string
|
||||||
|
) => {
|
||||||
|
const server = mcpServer.server;
|
||||||
|
|
||||||
|
// Set the notification handler
|
||||||
|
server.setNotificationHandler(
|
||||||
|
RootsListChangedNotificationSchema,
|
||||||
|
async () => {
|
||||||
|
try {
|
||||||
|
// Request the updated roots list from the client
|
||||||
|
const response = await server.listRoots();
|
||||||
|
if (response && "roots" in response) {
|
||||||
|
// Store the roots list for this client
|
||||||
|
roots.set(sessionId, response.roots);
|
||||||
|
|
||||||
|
// Notify the client of roots received
|
||||||
|
await server.sendLoggingMessage(
|
||||||
|
{
|
||||||
|
level: "info",
|
||||||
|
logger: "everything-server",
|
||||||
|
data: `Roots updated: ${response.roots.length} root(s) received from client`,
|
||||||
|
},
|
||||||
|
sessionId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
await server.sendLoggingMessage(
|
||||||
|
{
|
||||||
|
level: "error",
|
||||||
|
logger: "everything-server",
|
||||||
|
data: `Failed to request roots from client: ${
|
||||||
|
error instanceof Error ? error.message : String(error)
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
sessionId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
76
src/everything/tools/get-roots-list.ts
Normal file
76
src/everything/tools/get-roots-list.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||||
|
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
||||||
|
|
||||||
|
// Tool configuration
|
||||||
|
const name = "get-roots-list";
|
||||||
|
const config = {
|
||||||
|
title: "Get Roots List Tool",
|
||||||
|
description:
|
||||||
|
"Lists the current MCP roots provided by the client. Demonstrates the roots protocol capability even though this server doesn't access files.",
|
||||||
|
inputSchema: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers the 'get-roots-list' tool with the given MCP server.
|
||||||
|
*
|
||||||
|
* If the client does not support the roots protocol, the tool is not registered.
|
||||||
|
*
|
||||||
|
* The registered tool interacts with the MCP roots protocol, which enables the server to access information about
|
||||||
|
* the client's workspace directories or file system roots. When supported by the client, the server retrieves
|
||||||
|
* and formats the current list of roots for display.
|
||||||
|
*
|
||||||
|
* Key behaviors:
|
||||||
|
* - Determines whether the connected MCP client supports the roots protocol by checking client capabilities.
|
||||||
|
* - Fetches and formats the list of roots, including their names and URIs, if supported by the client.
|
||||||
|
* - Handles cases where roots are not supported, or no roots are currently provided, with explanatory messages.
|
||||||
|
*
|
||||||
|
* @param {McpServer} server - The server instance interacting with the MCP client and managing the roots protocol.
|
||||||
|
*/
|
||||||
|
export const registerGetRootsListTool = (server: McpServer) => {
|
||||||
|
const clientSupportsRoots =
|
||||||
|
server.server.getClientCapabilities()?.roots?.listChanged;
|
||||||
|
if (!clientSupportsRoots) {
|
||||||
|
server.registerTool(
|
||||||
|
name,
|
||||||
|
config,
|
||||||
|
async (args, extra): Promise<CallToolResult> => {
|
||||||
|
const currentRoots = (await server.server.listRoots()).roots;
|
||||||
|
if (currentRoots.length === 0) {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text:
|
||||||
|
"The client supports roots but no roots are currently configured.\n\n" +
|
||||||
|
"This could mean:\n" +
|
||||||
|
"1. The client hasn't provided any roots yet\n" +
|
||||||
|
"2. The client provided an empty roots list\n" +
|
||||||
|
"3. The roots configuration is still being loaded",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const rootsList = currentRoots
|
||||||
|
.map((root, index) => {
|
||||||
|
return `${index + 1}. ${root.name || "Unnamed Root"}\n URI: ${
|
||||||
|
root.uri
|
||||||
|
}`;
|
||||||
|
})
|
||||||
|
.join("\n\n");
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text:
|
||||||
|
`Current MCP Roots (${currentRoots.length} total):\n\n${rootsList}\n\n` +
|
||||||
|
"Note: This server demonstrates the roots protocol capability but doesn't actually access files. " +
|
||||||
|
"The roots are provided by the MCP client and can be used by servers that need file system access.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -4,6 +4,7 @@ import { registerEchoTool } from "./echo.js";
|
|||||||
import { registerGetEnvTool } from "./get-env.js";
|
import { registerGetEnvTool } from "./get-env.js";
|
||||||
import { registerGetResourceLinksTool } from "./get-resource-links.js";
|
import { registerGetResourceLinksTool } from "./get-resource-links.js";
|
||||||
import { registerGetResourceReferenceTool } from "./get-resource-reference.js";
|
import { registerGetResourceReferenceTool } from "./get-resource-reference.js";
|
||||||
|
import { registerGetRootsListTool } from "./get-roots-list.js";
|
||||||
import { registerGetStructuredContentTool } from "./get-structured-content.js";
|
import { registerGetStructuredContentTool } from "./get-structured-content.js";
|
||||||
import { registerGetSumTool } from "./get-sum.js";
|
import { registerGetSumTool } from "./get-sum.js";
|
||||||
import { registerGetTinyImageTool } from "./get-tiny-image.js";
|
import { registerGetTinyImageTool } from "./get-tiny-image.js";
|
||||||
@@ -11,8 +12,8 @@ import { registerGZipFileAsResourceTool } from "./gzip-file-as-resource.js";
|
|||||||
import { registerLongRunningOperationTool } from "./long-running-operation.js";
|
import { registerLongRunningOperationTool } from "./long-running-operation.js";
|
||||||
import { registerToggleLoggingTool } from "./toggle-logging.js";
|
import { registerToggleLoggingTool } from "./toggle-logging.js";
|
||||||
import { registerToggleSubscriberUpdatesTool } from "./toggle-subscriber-updates.js";
|
import { registerToggleSubscriberUpdatesTool } from "./toggle-subscriber-updates.js";
|
||||||
import { registerTriggerSamplingRequestTool } from "./trigger-sampling-request.js";
|
|
||||||
import { registerTriggerElicitationRequestTool } from "./trigger-elicitation-request.js";
|
import { registerTriggerElicitationRequestTool } from "./trigger-elicitation-request.js";
|
||||||
|
import { registerTriggerSamplingRequestTool } from "./trigger-sampling-request.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register the tools with the MCP server.
|
* Register the tools with the MCP server.
|
||||||
@@ -24,6 +25,7 @@ export const registerTools = (server: McpServer) => {
|
|||||||
registerGetEnvTool(server);
|
registerGetEnvTool(server);
|
||||||
registerGetResourceLinksTool(server);
|
registerGetResourceLinksTool(server);
|
||||||
registerGetResourceReferenceTool(server);
|
registerGetResourceReferenceTool(server);
|
||||||
|
registerGetRootsListTool(server);
|
||||||
registerGetStructuredContentTool(server);
|
registerGetStructuredContentTool(server);
|
||||||
registerGetSumTool(server);
|
registerGetSumTool(server);
|
||||||
registerGetTinyImageTool(server);
|
registerGetTinyImageTool(server);
|
||||||
|
|||||||
@@ -39,110 +39,122 @@ export const registerTriggerElicitationRequestTool = (server: McpServer) => {
|
|||||||
params: {
|
params: {
|
||||||
message: "Please provide inputs for the following fields:",
|
message: "Please provide inputs for the following fields:",
|
||||||
requestedSchema: {
|
requestedSchema: {
|
||||||
type: 'object',
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
name: {
|
name: {
|
||||||
title: 'String',
|
title: "String",
|
||||||
type: 'string',
|
type: "string",
|
||||||
description: 'Your full, legal name',
|
description: "Your full, legal name",
|
||||||
},
|
},
|
||||||
check: {
|
check: {
|
||||||
title: 'Boolean',
|
title: "Boolean",
|
||||||
type: 'boolean',
|
type: "boolean",
|
||||||
description: 'Agree to the terms and conditions',
|
description: "Agree to the terms and conditions",
|
||||||
},
|
},
|
||||||
firstLine: {
|
firstLine: {
|
||||||
title: 'String with default',
|
title: "String with default",
|
||||||
type: 'string',
|
type: "string",
|
||||||
description: 'Favorite first line of a story',
|
description: "Favorite first line of a story",
|
||||||
default: 'It was a dark and stormy night.',
|
default: "It was a dark and stormy night.",
|
||||||
},
|
},
|
||||||
email: {
|
email: {
|
||||||
title: 'String with email format',
|
title: "String with email format",
|
||||||
type: 'string',
|
type: "string",
|
||||||
format: 'email',
|
format: "email",
|
||||||
description: 'Your email address (will be verified, and never shared with anyone else)',
|
description:
|
||||||
|
"Your email address (will be verified, and never shared with anyone else)",
|
||||||
},
|
},
|
||||||
homepage: {
|
homepage: {
|
||||||
type: 'string',
|
type: "string",
|
||||||
format: 'uri',
|
format: "uri",
|
||||||
title: 'String with uri format',
|
title: "String with uri format",
|
||||||
description: 'Portfolio / personal website',
|
description: "Portfolio / personal website",
|
||||||
},
|
},
|
||||||
birthdate: {
|
birthdate: {
|
||||||
title: 'String with date format',
|
title: "String with date format",
|
||||||
type: 'string',
|
type: "string",
|
||||||
format: 'date',
|
format: "date",
|
||||||
description: 'Your date of birth',
|
description: "Your date of birth",
|
||||||
},
|
},
|
||||||
integer: {
|
integer: {
|
||||||
title: 'Integer',
|
title: "Integer",
|
||||||
type: 'integer',
|
type: "integer",
|
||||||
description: 'Your favorite integer (do not give us your phone number, pin, or other sensitive info)',
|
description:
|
||||||
|
"Your favorite integer (do not give us your phone number, pin, or other sensitive info)",
|
||||||
minimum: 1,
|
minimum: 1,
|
||||||
maximum: 100,
|
maximum: 100,
|
||||||
default: 42,
|
default: 42,
|
||||||
},
|
},
|
||||||
number: {
|
number: {
|
||||||
title: 'Number in range 1-1000',
|
title: "Number in range 1-1000",
|
||||||
type: 'number',
|
type: "number",
|
||||||
description: 'Favorite number (there are no wrong answers)',
|
description: "Favorite number (there are no wrong answers)",
|
||||||
minimum: 0,
|
minimum: 0,
|
||||||
maximum: 1000,
|
maximum: 1000,
|
||||||
default: 3.14,
|
default: 3.14,
|
||||||
},
|
},
|
||||||
untitledSingleSelectEnum: {
|
untitledSingleSelectEnum: {
|
||||||
type: 'string',
|
type: "string",
|
||||||
title: 'Untitled Single Select Enum',
|
title: "Untitled Single Select Enum",
|
||||||
description: 'Choose your favorite friend',
|
description: "Choose your favorite friend",
|
||||||
enum: ['Monica', 'Rachel', 'Joey', 'Chandler', 'Ross', 'Phoebe'],
|
enum: [
|
||||||
default: 'Monica'
|
"Monica",
|
||||||
|
"Rachel",
|
||||||
|
"Joey",
|
||||||
|
"Chandler",
|
||||||
|
"Ross",
|
||||||
|
"Phoebe",
|
||||||
|
],
|
||||||
|
default: "Monica",
|
||||||
},
|
},
|
||||||
untitledMultipleSelectEnum: {
|
untitledMultipleSelectEnum: {
|
||||||
type: 'array',
|
type: "array",
|
||||||
title: 'Untitled Multiple Select Enum',
|
title: "Untitled Multiple Select Enum",
|
||||||
description: 'Choose your favorite instruments',
|
description: "Choose your favorite instruments",
|
||||||
minItems: 1,
|
minItems: 1,
|
||||||
maxItems: 3,
|
maxItems: 3,
|
||||||
items: { type: 'string', enum: ['Guitar', 'Piano', 'Violin', 'Drums', 'Bass'] },
|
items: {
|
||||||
default: ['Guitar']
|
type: "string",
|
||||||
|
enum: ["Guitar", "Piano", "Violin", "Drums", "Bass"],
|
||||||
|
},
|
||||||
|
default: ["Guitar"],
|
||||||
},
|
},
|
||||||
titledSingleSelectEnum: {
|
titledSingleSelectEnum: {
|
||||||
type: 'string',
|
type: "string",
|
||||||
title: 'Titled Single Select Enum',
|
title: "Titled Single Select Enum",
|
||||||
description: 'Choose your favorite hero',
|
description: "Choose your favorite hero",
|
||||||
oneOf: [
|
oneOf: [
|
||||||
{ const: 'hero-1', title: 'Superman' },
|
{ const: "hero-1", title: "Superman" },
|
||||||
{ const: 'hero-2', title: 'Green Lantern' },
|
{ const: "hero-2", title: "Green Lantern" },
|
||||||
{ const: 'hero-3', title: 'Wonder Woman' }
|
{ const: "hero-3", title: "Wonder Woman" },
|
||||||
],
|
],
|
||||||
default: 'hero-1'
|
default: "hero-1",
|
||||||
},
|
},
|
||||||
titledMultipleSelectEnum: {
|
titledMultipleSelectEnum: {
|
||||||
type: 'array',
|
type: "array",
|
||||||
title: 'Titled Multiple Select Enum',
|
title: "Titled Multiple Select Enum",
|
||||||
description: 'Choose your favorite types of fish',
|
description: "Choose your favorite types of fish",
|
||||||
minItems: 1,
|
minItems: 1,
|
||||||
maxItems: 3,
|
maxItems: 3,
|
||||||
items: {
|
items: {
|
||||||
anyOf: [
|
anyOf: [
|
||||||
{ const: 'fish-1', title: 'Tuna' },
|
{ const: "fish-1", title: "Tuna" },
|
||||||
{ const: 'fish-2', title: 'Salmon' },
|
{ const: "fish-2", title: "Salmon" },
|
||||||
{ const: 'fish-3', title: 'Trout' }
|
{ const: "fish-3", title: "Trout" },
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
default: ['fish-1']
|
default: ["fish-1"],
|
||||||
},
|
},
|
||||||
legacyTitledEnum: {
|
legacyTitledEnum: {
|
||||||
type: 'string',
|
type: "string",
|
||||||
title: 'Legacy Titled Single Select Enum',
|
title: "Legacy Titled Single Select Enum",
|
||||||
description: 'Choose your favorite type of pet',
|
description: "Choose your favorite type of pet",
|
||||||
enum: ['pet-1', 'pet-2', 'pet-3', 'pet-4', 'pet-5'],
|
enum: ["pet-1", "pet-2", "pet-3", "pet-4", "pet-5"],
|
||||||
enumNames: ['Cats', 'Dogs', 'Birds', 'Fish', 'Reptiles'],
|
enumNames: ["Cats", "Dogs", "Birds", "Fish", "Reptiles"],
|
||||||
default: 'pet-1',
|
default: "pet-1",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
required: ['name'],
|
required: ["name"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ const transports: Map<string, SSEServerTransport> = new Map<
|
|||||||
|
|
||||||
app.get("/sse", async (req, res) => {
|
app.get("/sse", async (req, res) => {
|
||||||
let transport: SSEServerTransport;
|
let transport: SSEServerTransport;
|
||||||
const { server, cleanup } = createServer();
|
const { server, clientConnected, cleanup } = createServer();
|
||||||
|
|
||||||
if (req?.query?.sessionId) {
|
if (req?.query?.sessionId) {
|
||||||
const sessionId = req?.query?.sessionId as string;
|
const sessionId = req?.query?.sessionId as string;
|
||||||
@@ -38,6 +38,8 @@ app.get("/sse", async (req, res) => {
|
|||||||
// Connect server to transport
|
// Connect server to transport
|
||||||
await server.connect(transport);
|
await server.connect(transport);
|
||||||
const sessionId = transport.sessionId;
|
const sessionId = transport.sessionId;
|
||||||
|
clientConnected(sessionId);
|
||||||
|
|
||||||
console.error("Client Connected: ", sessionId);
|
console.error("Client Connected: ", sessionId);
|
||||||
|
|
||||||
// Handle close of connection
|
// Handle close of connection
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ console.error("Starting default (STDIO) server...");
|
|||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const transport = new StdioServerTransport();
|
const transport = new StdioServerTransport();
|
||||||
const { server, cleanup } = createServer();
|
const { server, clientConnected, cleanup } = createServer();
|
||||||
|
|
||||||
await server.connect(transport);
|
await server.connect(transport);
|
||||||
|
clientConnected();
|
||||||
// Cleanup on exit
|
// Cleanup on exit
|
||||||
process.on("SIGINT", async () => {
|
process.on("SIGINT", async () => {
|
||||||
await server.close();
|
await server.close();
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ app.post("/mcp", async (req: Request, res: Response) => {
|
|||||||
// Reuse existing transport
|
// Reuse existing transport
|
||||||
transport = transports.get(sessionId)!;
|
transport = transports.get(sessionId)!;
|
||||||
} else if (!sessionId) {
|
} else if (!sessionId) {
|
||||||
const { server, cleanup } = createServer();
|
const { server, clientConnected, cleanup } = createServer();
|
||||||
|
|
||||||
// New initialization request
|
// New initialization request
|
||||||
const eventStore = new InMemoryEventStore();
|
const eventStore = new InMemoryEventStore();
|
||||||
@@ -47,6 +47,7 @@ app.post("/mcp", async (req: Request, res: Response) => {
|
|||||||
// This avoids race conditions where requests might come in before the session is stored
|
// This avoids race conditions where requests might come in before the session is stored
|
||||||
console.log(`Session initialized with ID: ${sessionId}`);
|
console.log(`Session initialized with ID: ${sessionId}`);
|
||||||
transports.set(sessionId, transport);
|
transports.set(sessionId, transport);
|
||||||
|
clientConnected(sessionId);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user