From 320e3d8b254f174ce36c461b69fa962ac24522c5 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Tue, 9 Dec 2025 17:47:38 -0500 Subject: [PATCH] [WIP] Refactor everything server to be more modular and use recommended APIs. Adding the get-resource-reference tool * Updated architecture.md * In prompts/resource.ts - Refactor/extracted the prompt argument completers into exported functions in resources/templates.ts - Refactor/extracted BLOB_TYPE, TEXT_TYPE, and resourceTypes into exported constants in resources/templates.ts as RESOURCE_TYPE_BLOB, RESOURCE_TYPE_TEXT, and RESOURCE_TYPES - In resources/templates.ts - refactor renamed index to resourceId throughout for consistency with prompts and tool references * Added tools/get-resource-reference.ts - Registers the 'get-resource-reference' tool with the provided McpServer instance. - uses enum and number schema for tools to provide resourceType and resourceId arguments. Completables don't work for tool arguments. - Returns the corresponding dynamic resource * In tools/index.ts - imported registerGetResourceReferenceTool - in registerTools - called registerGetResourceReferenceTool --- src/everything/prompts/resource.ts | 54 +++--- src/everything/resources/templates.ts | 173 +++++++++++++----- .../tools/get-resource-reference.ts | 98 ++++++++++ src/everything/tools/index.ts | 4 +- 4 files changed, 253 insertions(+), 76 deletions(-) create mode 100644 src/everything/tools/get-resource-reference.ts diff --git a/src/everything/prompts/resource.ts b/src/everything/prompts/resource.ts index 82a06e56..03989aaa 100644 --- a/src/everything/prompts/resource.ts +++ b/src/everything/prompts/resource.ts @@ -1,11 +1,16 @@ -import { z } from "zod"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { completable } from "@modelcontextprotocol/sdk/server/completable.js"; +import { + resourceTypeCompleter, + resourceIdForPromptCompleter, +} from "../resources/templates.js"; import { textResource, textResourceUri, blobResourceUri, blobResource, + RESOURCE_TYPE_BLOB, + RESOURCE_TYPE_TEXT, + RESOURCE_TYPES, } from "../resources/templates.js"; /** @@ -16,29 +21,10 @@ import { * @param server */ export const registerEmbeddedResourcePrompt = (server: McpServer) => { - // Resource types - const BLOB_TYPE = "Blob"; - const TEXT_TYPE = "Text"; - const resourceTypes = [BLOB_TYPE, TEXT_TYPE]; - // Prompt arguments const promptArgsSchema = { - resourceType: completable( - z.string().describe("Type of resource to fetch"), - (value: string) => { - return [TEXT_TYPE, BLOB_TYPE].filter((t) => t.startsWith(value)); - } - ), - // NOTE: Currently, prompt arguments can only be strings since type is not field of PromptArgument - // Consequently, we must define it as a string and convert the argument to number before using it - // https://modelcontextprotocol.io/specification/2025-11-25/schema#promptargument - resourceId: completable( - z.string().describe("ID of the text resource to fetch"), - (value: string) => { - const resourceId = Number(value); - return Number.isInteger(resourceId) ? [value] : []; - } - ), + resourceType: resourceTypeCompleter, + resourceId: resourceIdForPromptCompleter, }; // Register the prompt @@ -51,28 +37,36 @@ export const registerEmbeddedResourcePrompt = (server: McpServer) => { }, (args) => { // Validate resource type argument - const { resourceType } = args; - if (!resourceTypes.includes(resourceType)) { + const resourceType = args.resourceType; + if ( + !RESOURCE_TYPES.includes( + resourceType as typeof RESOURCE_TYPE_TEXT | typeof RESOURCE_TYPE_BLOB + ) + ) { throw new Error( - `Invalid resourceType: ${args?.resourceType}. Must be ${TEXT_TYPE} or ${BLOB_TYPE}.` + `Invalid resourceType: ${args?.resourceType}. Must be ${RESOURCE_TYPE_TEXT} or ${RESOURCE_TYPE_BLOB}.` ); } // Validate resourceId argument const resourceId = Number(args?.resourceId); - if (!Number.isFinite(resourceId) || !Number.isInteger(resourceId)) { + if ( + !Number.isFinite(resourceId) || + !Number.isInteger(resourceId) || + resourceId < 1 + ) { throw new Error( - `Invalid resourceId: ${args?.resourceId}. Must be a finite integer.` + `Invalid resourceId: ${args?.resourceId}. Must be a finite positive integer.` ); } // Get resource based on the resource type const uri = - resourceType === TEXT_TYPE + resourceType === RESOURCE_TYPE_TEXT ? textResourceUri(resourceId) : blobResourceUri(resourceId); const resource = - resourceType === TEXT_TYPE + resourceType === RESOURCE_TYPE_TEXT ? textResource(uri, resourceId) : blobResource(uri, resourceId); diff --git a/src/everything/resources/templates.ts b/src/everything/resources/templates.ts index 79279428..db509706 100644 --- a/src/everything/resources/templates.ts +++ b/src/everything/resources/templates.ts @@ -1,26 +1,94 @@ +import { z } from "zod"; import { + CompleteResourceTemplateCallback, McpServer, ResourceTemplate, } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { completable } from "@modelcontextprotocol/sdk/server/completable.js"; + +// Resource types +export const RESOURCE_TYPE_TEXT = "Text" as const; +export const RESOURCE_TYPE_BLOB = "Blob" as const; +export const RESOURCE_TYPES: string[] = [ + RESOURCE_TYPE_TEXT, + RESOURCE_TYPE_BLOB, +]; + +/** + * A completer function for resource types. + * + * This variable provides functionality to perform autocompletion for the resource types based on user input. + * It uses a schema description to validate the input and filters through a predefined list of resource types + * to return suggestions that start with the given input. + * + * The input value is expected to be a string representing the type of resource to fetch. + * The completion logic matches the input against available resource types. + */ +export const resourceTypeCompleter = completable( + z.string().describe("Type of resource to fetch"), + (value: string) => { + return RESOURCE_TYPES.filter((t) => t.startsWith(value)); + } +); + +/** + * A completer function for resource IDs as strings. + * + * The `resourceIdCompleter` accepts a string input representing the ID of a text resource + * and validates whether the provided value corresponds to an integer resource ID. + * + * NOTE: Currently, prompt arguments can only be strings since type is not field of `PromptArgument` + * Consequently, we must define it as a string and convert the argument to number before using it + * https://modelcontextprotocol.io/specification/2025-11-25/schema#promptargument + * + * If the value is a valid integer, it returns the value within an array. + * Otherwise, it returns an empty array. + * + * The input string is first transformed into a number and checked to ensure it is an integer. + * This helps validate and suggest appropriate resource IDs. + */ +export const resourceIdForPromptCompleter = completable( + z.string().describe("ID of the text resource to fetch"), + (value: string) => { + const resourceId = Number(value); + return Number.isInteger(resourceId) && resourceId > 0 ? [value] : []; + } +); + +/** + * A callback function that acts as a completer for resource ID values, validating and returning + * the input value as part of a resource template. + * + * @typedef {CompleteResourceTemplateCallback} + * @param {string} value - The input string value to be evaluated as a resource ID. + * @returns {string[]} Returns an array containing the input value if it represents a positive + * integer resource ID, otherwise returns an empty array. + */ +export const resourceIdForResourceTemplateCompleter: CompleteResourceTemplateCallback = + (value: string) => { + const resourceId = Number(value); + + return Number.isInteger(resourceId) && resourceId > 0 ? [value] : []; + }; const uriBase: string = "demo://resource/dynamic"; const textUriBase: string = `${uriBase}/text`; const blobUriBase: string = `${uriBase}/blob`; -const textUriTemplate: string = `${textUriBase}/{index}`; -const blobUriTemplate: string = `${blobUriBase}/{index}`; +const textUriTemplate: string = `${textUriBase}/{resourceId}`; +const blobUriTemplate: string = `${blobUriBase}/{resourceId}`; /** * Create a dynamic text resource * - Exposed for use by embedded resource prompt example * @param uri - * @param index + * @param resourceId */ -export const textResource = (uri: URL, index: number) => { +export const textResource = (uri: URL, resourceId: number) => { const timestamp = new Date().toLocaleTimeString(); return { uri: uri.toString(), mimeType: "text/plain", - text: `Resource ${index}: This is a plaintext resource created at ${timestamp}`, + text: `Resource ${resourceId}: This is a plaintext resource created at ${timestamp}`, }; }; @@ -28,12 +96,12 @@ export const textResource = (uri: URL, index: number) => { * Create a dynamic blob resource * - Exposed for use by embedded resource prompt example * @param uri - * @param index + * @param resourceId */ -export const blobResource = (uri: URL, index: number) => { +export const blobResource = (uri: URL, resourceId: number) => { const timestamp = new Date().toLocaleTimeString(); const resourceText = Buffer.from( - `Resource ${index}: This is a base64 blob created at ${timestamp}` + `Resource ${resourceId}: This is a base64 blob created at ${timestamp}` ).toString("base64"); return { uri: uri.toString(), @@ -45,66 +113,78 @@ export const blobResource = (uri: URL, index: number) => { /** * Create a dynamic text resource URI * - Exposed for use by embedded resource prompt example - * @param index + * @param resourceId */ -export const textResourceUri = (index: number) => - new URL(`${textUriBase}/${index}`); +export const textResourceUri = (resourceId: number) => + new URL(`${textUriBase}/${resourceId}`); /** * Create a dynamic blob resource URI * - Exposed for use by embedded resource prompt example - * @param index + * @param resourceId */ -export const blobResourceUri = (index: number) => - new URL(`${blobUriBase}/${index}`); +export const blobResourceUri = (resourceId: number) => + new URL(`${blobUriBase}/${resourceId}`); + +/** + * Parses the resource identifier from the provided URI and validates it + * against the given variables. Throws an error if the URI corresponds + * to an unknown resource or if the resource identifier is invalid. + * + * @param {URL} uri - The URI of the resource to be parsed. + * @param {Record} variables - A record containing context-specific variables that include the resourceId. + * @returns {number} The parsed and validated resource identifier as an integer. + * @throws {Error} Throws an error if the URI matches unsupported base URIs or if the resourceId is invalid. + */ +const parseResourceId = (uri: URL, variables: Record) => { + const uriError = `Unknown resource: ${uri.toString()}`; + if ( + uri.toString().startsWith(textUriBase) && + uri.toString().startsWith(blobUriBase) + ) { + throw new Error(uriError); + } else { + const idxStr = String((variables as any).resourceId ?? ""); + const idx = Number(idxStr); + if (Number.isFinite(idx) && Number.isInteger(idx) && idx > 0) { + return idx; + } else { + throw new Error(uriError); + } + } +}; /** * Register resource templates with the MCP server. - * - Text and blob resources, dynamically generated from the URI {index} variable - * - Any finite integer is acceptable for the index variable + * - Text and blob resources, dynamically generated from the URI {resourceId} variable + * - Any finite positive integer is acceptable for the resourceId variable * - List resources method will not return these resources * - These are only accessible via template URIs * - Both blob and text resources: * - have content that is dynamically generated, including a timestamp * - have different template URIs - * - Blob: "demo://resource/dynamic/blob/{index}" - * - Text: "demo://resource/dynamic/text/{index}" + * - Blob: "demo://resource/dynamic/blob/{resourceId}" + * - Text: "demo://resource/dynamic/text/{resourceId}" * * @param server */ export const registerResourceTemplates = (server: McpServer) => { - // Parse the index from the URI - const parseIndex = (uri: URL, variables: Record) => { - const uriError = `Unknown resource: ${uri.toString()}`; - if ( - uri.toString().startsWith(textUriBase) && - uri.toString().startsWith(blobUriBase) - ) { - throw new Error(uriError); - } else { - const idxStr = String((variables as any).index ?? ""); - const idx = Number(idxStr); - if (Number.isFinite(idx) && Number.isInteger(idx)) { - return idx; - } else { - throw new Error(uriError); - } - } - }; - // Register the text resource template server.registerResource( "Dynamic Text Resource", - new ResourceTemplate(textUriTemplate, { list: undefined }), + new ResourceTemplate(textUriTemplate, { + list: undefined, + complete: { resourceId: resourceIdForResourceTemplateCompleter }, + }), { mimeType: "text/plain", description: - "Plaintext dynamic resource fabricated from the {index} variable, which must be an integer.", + "Plaintext dynamic resource fabricated from the {resourceId} variable, which must be an integer.", }, async (uri, variables) => { - const index = parseIndex(uri, variables); + const resourceId = parseResourceId(uri, variables); return { - contents: [textResource(uri, index)], + contents: [textResource(uri, resourceId)], }; } ); @@ -112,16 +192,19 @@ export const registerResourceTemplates = (server: McpServer) => { // Register the blob resource template server.registerResource( "Dynamic Blob Resource", - new ResourceTemplate(blobUriTemplate, { list: undefined }), + new ResourceTemplate(blobUriTemplate, { + list: undefined, + complete: { resourceId: resourceIdForResourceTemplateCompleter }, + }), { mimeType: "application/octet-stream", description: - "Binary (base64) dynamic resource fabricated from the {index} variable, which must be an integer.", + "Binary (base64) dynamic resource fabricated from the {resourceId} variable, which must be an integer.", }, async (uri, variables) => { - const index = parseIndex(uri, variables); + const resourceId = parseResourceId(uri, variables); return { - contents: [blobResource(uri, index)], + contents: [blobResource(uri, resourceId)], }; } ); diff --git a/src/everything/tools/get-resource-reference.ts b/src/everything/tools/get-resource-reference.ts new file mode 100644 index 00000000..838d9a15 --- /dev/null +++ b/src/everything/tools/get-resource-reference.ts @@ -0,0 +1,98 @@ +import { z } from "zod"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import { + textResource, + textResourceUri, + blobResourceUri, + blobResource, + RESOURCE_TYPE_BLOB, + RESOURCE_TYPE_TEXT, + RESOURCE_TYPES, +} from "../resources/templates.js"; + +// Tool input schema +const GetResourceReferenceSchema = z.object({ + resourceType: z + .enum([RESOURCE_TYPE_TEXT, RESOURCE_TYPE_BLOB]) + .default(RESOURCE_TYPE_TEXT), + resourceId: z + .number() + .default(1) + .describe("ID of the text resource to fetch"), +}); + +// Tool configuration +const name = "get-resource-reference"; +const config = { + title: "Get Resource Reference Tool", + description: "Adds two numbers", + inputSchema: GetResourceReferenceSchema, +}; + +/** + * Registers the 'get-resource-reference' tool with the provided McpServer instance. + * + * The registered tool validates and processes arguments for retrieving a resource + * reference. Supported resource types include predefined `RESOURCE_TYPE_TEXT` and + * `RESOURCE_TYPE_BLOB`. The retrieved resource's reference will include the resource + * ID, type, and its associated URI. + * + * The tool performs the following operations: + * 1. Validates the `resourceType` argument to ensure it matches a supported type. + * 2. Validates the `resourceId` argument to ensure it is a finite positive integer. + * 3. Constructs a URI for the resource based on its type (text or blob). + * 4. Retrieves the resource and returns it in a structured response object. + * + * @param {McpServer} server - The server instance where the tool is registered. + */ +export const registerGetResourceReferenceTool = (server: McpServer) => { + server.registerTool(name, config, async (args): Promise => { + // Validate resource type argument + const { resourceType } = args; + if (!RESOURCE_TYPES.includes(resourceType)) { + throw new Error( + `Invalid resourceType: ${args?.resourceType}. Must be ${RESOURCE_TYPE_TEXT} or ${RESOURCE_TYPE_BLOB}.` + ); + } + + // Validate resourceId argument + const resourceId = Number(args?.resourceId); + if ( + !Number.isFinite(resourceId) || + !Number.isInteger(resourceId) || + resourceId < 1 + ) { + throw new Error( + `Invalid resourceId: ${args?.resourceId}. Must be a finite positive integer.` + ); + } + + // Get resource based on the resource type + const uri = + resourceType === RESOURCE_TYPE_TEXT + ? textResourceUri(resourceId) + : blobResourceUri(resourceId); + const resource = + resourceType === RESOURCE_TYPE_TEXT + ? textResource(uri, resourceId) + : blobResource(uri, resourceId); + + return { + content: [ + { + type: "text", + text: `Returning resource reference for Resource ${resourceId}:`, + }, + { + type: "resource", + resource: resource, + }, + { + type: "text", + text: `You can access this resource using the URI: ${resource.uri}`, + }, + ], + }; + }); +}; diff --git a/src/everything/tools/index.ts b/src/everything/tools/index.ts index bccd41da..41b2f720 100644 --- a/src/everything/tools/index.ts +++ b/src/everything/tools/index.ts @@ -1,13 +1,14 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { registerAddTool } from "./add.js"; +import { registerAnnotatedMessageTool } from "./annotated-message.js"; import { registerEchoTool } from "./echo.js"; import { registerGetTinyImageTool } from "./get-tiny-image.js"; +import { registerGetResourceReferenceTool } from "./get-resource-reference.js"; import { registerLongRunningOperationTool } from "./long-running-operation.js"; import { registerPrintEnvTool } from "./print-env.js"; import { registerSamplingRequestTool } from "./sampling-request.js"; import { registerToggleLoggingTool } from "./toggle-logging.js"; import { registerToggleSubscriberUpdatesTool } from "./toggle-subscriber-updates.js"; -import {registerAnnotatedMessageTool} from "./annotated-message.js"; /** * Register the tools with the MCP server. @@ -18,6 +19,7 @@ export const registerTools = (server: McpServer) => { registerAnnotatedMessageTool(server); registerEchoTool(server); registerGetTinyImageTool(server); + registerGetResourceReferenceTool(server); registerLongRunningOperationTool(server); registerPrintEnvTool(server); registerSamplingRequestTool(server);