diff --git a/src/everything/docs/architecture.md b/src/everything/docs/architecture.md index e765833a..eec41428 100644 --- a/src/everything/docs/architecture.md +++ b/src/everything/docs/architecture.md @@ -30,10 +30,11 @@ src/everything │ ├── index.ts │ ├── simple.ts │ ├── complex.ts -│ └── completions.ts +│ ├── completions.ts +│ └── resource.ts ├── resources │ ├── index.ts -│ ├── dynamic.ts +│ ├── template.ts │ └── static.ts ├── docs │ ├── server-instructions.md @@ -87,16 +88,19 @@ At `src/everything`: - Registers `complex-prompt`: a prompt with two arguments (`city` required, `state` optional) used to compose a message. - completions.ts - Registers `completable-prompt`: a prompt whose arguments support server-driven completions using the SDK’s `completable(...)` helper (e.g., completing `department` and context-aware `name`). + - resource.ts + - Exposes `registerEmbeddedResourcePrompt(server)` which registers `resource-prompt` — a prompt that accepts `resourceId` and embeds a dynamically generated text resource within the returned messages. Internally reuses helpers from `resources/template.ts`. - resources/ - index.ts - - `registerResources(server)` orchestrator; delegates to static and dynamic resources. - - dynamic.ts + - `registerResources(server)` orchestrator; delegates to template‑based dynamic resources and static resources by calling `registerResourceTemplates(server)` and `registerStaticResources(server)`. + - template.ts - Registers two dynamic, template‑driven resources using `ResourceTemplate`: - Text: `demo://resource/dynamic/text/{index}` (MIME: `text/plain`) - Blob: `demo://resource/dynamic/blob/{index}` (MIME: `application/octet-stream`, Base64 payload) - - The `{index}` path variable must be a finite integer. Content is generated on demand with a GMT timestamp. + - The `{index}` path variable must be a finite integer. Content is generated on demand with a timestamp. + - Exposes helpers `textResource(uri, index)` and `textResourceUri(index)` so other modules can construct and embed text resources directly (e.g., from prompts). - static.ts - Registers static resources for each file in the `docs/` folder. - URIs follow the pattern: `demo://static/docs/`. @@ -157,6 +161,7 @@ At `src/everything`: - `simple-prompt` (prompts/simple.ts): No-argument prompt that returns a static user message. - `complex-prompt` (prompts/complex.ts): Two-argument prompt with `city` (required) and `state` (optional) used to compose a question. - `completable-prompt` (prompts/completions.ts): Demonstrates argument auto-completions with the SDK’s `completable` helper; `department` completions drive context-aware `name` suggestions. + - `resource-prompt` (prompts/resource.ts): Accepts `resourceId` (string convertible to integer) and returns messages that include an embedded dynamic text resource generated via `resources/template.ts`. - Resources - Dynamic Text: `demo://resource/dynamic/text/{index}` (content generated on the fly) diff --git a/src/everything/prompts/index.ts b/src/everything/prompts/index.ts index 51d1dc57..32dcda22 100644 --- a/src/everything/prompts/index.ts +++ b/src/everything/prompts/index.ts @@ -2,6 +2,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { registerSimplePrompt } from "./simple.js"; import { registerComplexPrompt } from "./complex.js"; import { registerPromptWithCompletions } from "./completions.js"; +import { registerEmbeddedResourcePrompt } from "./resource.js" /** * Register the prompts with the MCP server. @@ -11,4 +12,5 @@ export const registerPrompts = (server: McpServer) => { registerSimplePrompt(server); registerComplexPrompt(server); registerPromptWithCompletions(server); + registerEmbeddedResourcePrompt(server); }; diff --git a/src/everything/prompts/resource.ts b/src/everything/prompts/resource.ts new file mode 100644 index 00000000..6f9ee1ce --- /dev/null +++ b/src/everything/prompts/resource.ts @@ -0,0 +1,51 @@ +import { z } from "zod"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import {textResource, textResourceUri} from "../resources/template.js"; + +export const registerEmbeddedResourcePrompt = (server: McpServer) => { + // 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 + const promptArgsSchema = { + resourceId: z.string().describe("ID of the text resource to fetch"), + }; + + server.registerPrompt( + "resource-prompt", + { + title: "Resource Prompt", + description: "A prompt that includes an embedded resource reference", + argsSchema: promptArgsSchema, + }, + (args) => { + const resourceId = Number(args?.resourceId); // Inspector sends strings only + if (!Number.isFinite(resourceId) || !Number.isInteger(resourceId)) { + throw new Error( + `Invalid resourceId: ${args?.resourceId}. Must be a finite integer.` + ); + } + + const uri = textResourceUri(resourceId); + const resource = textResource(uri, resourceId); + + return { + messages: [ + { + role: "user", + content: { + type: "text", + text: `This prompt includes the text resource with id: ${resourceId}. Please analyze the following resource:`, + }, + }, + { + role: "user", + content: { + type: "resource", + resource: resource, + }, + }, + ], + }; + } + ); +}; diff --git a/src/everything/resources/index.ts b/src/everything/resources/index.ts index 481c9386..21033caa 100644 --- a/src/everything/resources/index.ts +++ b/src/everything/resources/index.ts @@ -1,5 +1,5 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { registerDynamicResources } from "./dynamic.js"; +import { registerResourceTemplates } from "./template.js"; import { registerStaticResources } from "./static.js"; /** @@ -7,6 +7,6 @@ import { registerStaticResources } from "./static.js"; * @param server */ export const registerResources = (server: McpServer) => { - registerDynamicResources(server); + registerResourceTemplates(server); registerStaticResources(server); }; diff --git a/src/everything/resources/dynamic.ts b/src/everything/resources/template.ts similarity index 54% rename from src/everything/resources/dynamic.ts rename to src/everything/resources/template.ts index b4409f12..570ef6a1 100644 --- a/src/everything/resources/dynamic.ts +++ b/src/everything/resources/template.ts @@ -3,8 +3,52 @@ import { ResourceTemplate, } from "@modelcontextprotocol/sdk/server/mcp.js"; +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}`; + /** - * Register dynamic resources with the MCP server. + * Create a dynamic text resource + * @param uri + * @param index + */ +export const textResource = (uri: URL, index: number) => { + const timestamp = new Date().toLocaleTimeString(); + return { + uri: uri.toString(), + mimeType: "text/plain", + text: `Resource ${index}: This is a plaintext resource created at ${timestamp}`, + }; +}; + +/** + * Create a dynamic blob resource + * @param uri + * @param index + */ +export const blobResource = (uri: URL, index: number) => { + const timestamp = new Date().toLocaleTimeString(); + const resourceText = Buffer.from( + `Resource ${index}: This is a base64 blob created at ${timestamp}` + ).toString("base64"); + return { + uri: uri.toString(), + mimeType: "text/plain", + text: resourceText, + }; +}; + +/** + * Create a dynamic text resource URI + * @param index + */ +export const textResourceUri = (index: number) => + new URL(`${textUriBase}/${index}`); + +/** + * 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 @@ -18,41 +62,7 @@ import { * * @param server */ -export const registerDynamicResources = (server: McpServer) => { - 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}`; - - // Format a GMT timestamp like "7:30AM GMT on November 3" - const formatGmtTimestamp = () => { - const d = new Date(); - const h24 = d.getUTCHours(); - const minutes = d.getUTCMinutes(); - const ampm = h24 >= 12 ? "PM" : "AM"; - let h12 = h24 % 12; - if (h12 === 0) h12 = 12; - const mm = String(minutes).padStart(2, "0"); - const months = [ - "January", - "February", - "March", - "April", - "May", - "June", - "July", - "August", - "September", - "October", - "November", - "December", - ]; - const monthName = months[d.getUTCMonth()]; - const day = d.getUTCDate(); - return `${h12}:${mm}${ampm} GMT on ${monthName} ${day}`; - }; - +export const registerResourceTemplates = (server: McpServer) => { const parseIndex = (uri: URL, variables: Record) => { const uriError = `Unknown resource: ${uri.toString()}`; if ( @@ -71,7 +81,7 @@ export const registerDynamicResources = (server: McpServer) => { } }; - // Text resource registration + // Text resource template registration server.registerResource( "Dynamic Text Resource", new ResourceTemplate(textUriTemplate, { list: undefined }), @@ -83,18 +93,12 @@ export const registerDynamicResources = (server: McpServer) => { async (uri, variables) => { const index = parseIndex(uri, variables); return { - contents: [ - { - uri: uri.toString(), - mimeType: "text/plain", - text: `Resource ${index}: This is a plaintext resource created at ${formatGmtTimestamp()}`, - }, - ], + contents: [textResource(uri, index)], }; } ); - // Blob resource registration + // Blob resource template registration server.registerResource( "Dynamic Blob Resource", new ResourceTemplate(blobUriTemplate, { list: undefined }), @@ -105,17 +109,8 @@ export const registerDynamicResources = (server: McpServer) => { }, async (uri, variables) => { const index = parseIndex(uri, variables); - const buffer = Buffer.from( - `Resource ${index}: This is a base64 blob created at ${formatGmtTimestamp()}` - ); return { - contents: [ - { - uri: uri.toString(), - mimeType: "application/octet-stream", - blob: buffer.toString("base64"), - }, - ], + contents: [blobResource(uri, index)], }; } );