diff --git a/src/everything/docs/architecture.md b/src/everything/docs/architecture.md index eec41428..cfa084d3 100644 --- a/src/everything/docs/architecture.md +++ b/src/everything/docs/architecture.md @@ -29,13 +29,13 @@ src/everything ├── prompts │ ├── index.ts │ ├── simple.ts -│ ├── complex.ts +│ ├── args.ts │ ├── completions.ts │ └── resource.ts ├── resources │ ├── index.ts -│ ├── template.ts -│ └── static.ts +│ ├── templates.ts +│ └── files.ts ├── docs │ ├── server-instructions.md │ └── architecture.md @@ -84,26 +84,26 @@ At `src/everything`: - `registerPrompts(server)` orchestrator; delegates to individual prompt registrations. - simple.ts - Registers `simple-prompt`: a prompt with no arguments that returns a single user message. - - complex.ts - - Registers `complex-prompt`: a prompt with two arguments (`city` required, `state` optional) used to compose a message. + - args.ts + - Registers `args-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`. + - Exposes `registerEmbeddedResourcePrompt(server)` which registers `resource-prompt` — a prompt that accepts `resourceType` ("Text" or "Blob") and `resourceId` (integer), and embeds a dynamically generated resource of the requested type within the returned messages. Internally reuses helpers from `resources/templates.ts`. - resources/ - index.ts - - `registerResources(server)` orchestrator; delegates to template‑based dynamic resources and static resources by calling `registerResourceTemplates(server)` and `registerStaticResources(server)`. - - template.ts + - `registerResources(server)` orchestrator; delegates to template‑based dynamic resources and static file-based resources by calling `registerResourceTemplates(server)` and `registerFileResources(server)`. + - templates.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 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/`. + - Exposes helpers `textResource(uri, index)`, `textResourceUri(index)`, `blobResource(uri, index)`, and `blobResourceUri(index)` so other modules can construct and embed dynamic resources directly (e.g., from prompts). + - files.ts + - Registers static file-based resources for each file in the `docs/` folder. + - URIs follow the pattern: `demo://resource/static/document/`. - Serves markdown files as `text/markdown`, `.txt` as `text/plain`, `.json` as `application/json`, others default to `text/plain`. - docs/ @@ -159,14 +159,14 @@ At `src/everything`: - Prompts - `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. + - `args-prompt` (prompts/args.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`. + - `resource-prompt` (prompts/resource.ts): Accepts `resourceType` ("Text" or "Blob") and `resourceId` (string convertible to integer) and returns messages that include an embedded dynamic resource of the selected type generated via `resources/templates.ts`. - Resources - Dynamic Text: `demo://resource/dynamic/text/{index}` (content generated on the fly) - Dynamic Blob: `demo://resource/dynamic/blob/{index}` (base64 payload generated on the fly) - - Static Docs: `demo://static/docs/` (serves files from `src/everything/docs/` as static resources) + - Static Docs: `demo://resource/static/document/` (serves files from `src/everything/docs/` as static file-based resources) ## Extension Points diff --git a/src/everything/prompts/complex.ts b/src/everything/prompts/args.ts similarity index 69% rename from src/everything/prompts/complex.ts rename to src/everything/prompts/args.ts index c0d38348..7e445a4c 100644 --- a/src/everything/prompts/complex.ts +++ b/src/everything/prompts/args.ts @@ -1,16 +1,25 @@ import { z } from "zod"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -export const registerComplexPrompt = (server: McpServer) => { +/** + * Register a prompt with arguments + * - Two arguments, one required and one optional + * - Combines argument values in the returned prompt + * + * @param server + */ +export const registerArgumentsPrompt = (server: McpServer) => { + // Prompt arguments const promptArgsSchema = { city: z.string().describe("Name of the city"), state: z.string().describe("Name of the state").optional(), }; + // Register the prompt server.registerPrompt( - "complex-prompt", + "args-prompt", { - title: "Complex Prompt", + title: "Arguments Prompt", description: "A prompt with two arguments, one required and one optional", argsSchema: promptArgsSchema, }, diff --git a/src/everything/prompts/completions.ts b/src/everything/prompts/completions.ts index 23301ba8..e47c36e5 100644 --- a/src/everything/prompts/completions.ts +++ b/src/everything/prompts/completions.ts @@ -2,36 +2,54 @@ import { z } from "zod"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { completable } from "@modelcontextprotocol/sdk/server/completable.js"; +/** + * Register a prompt with completable arguments + * - Two required arguments, both with completion handlers + * - First argument value will be included in context for second argument + * - Allows second argument to depend on the first argument value + * + * @param server + */ export const registerPromptWithCompletions = (server: McpServer) => { + // Prompt arguments const promptArgsSchema = { - department: completable(z.string(), (value) => { - return ["Engineering", "Sales", "Marketing", "Support"].filter((d) => - d.startsWith(value) - ); - }), - name: completable(z.string(), (value, context) => { - const department = context?.arguments?.["department"]; - if (department === "Engineering") { - return ["Alice", "Bob", "Charlie"].filter((n) => n.startsWith(value)); - } else if (department === "Sales") { - return ["David", "Eve", "Frank"].filter((n) => n.startsWith(value)); - } else if (department === "Marketing") { - return ["Grace", "Henry", "Iris"].filter((n) => n.startsWith(value)); - } else if (department === "Support") { - return ["John", "Kim", "Lee"].filter((n) => n.startsWith(value)); + department: completable( + z.string().describe("Choose the department."), + (value) => { + return ["Engineering", "Sales", "Marketing", "Support"].filter((d) => + d.startsWith(value) + ); } - return []; - }), + ), + name: completable( + z + .string() + .describe("Choose a team member to lead the selected department."), + (value, context) => { + const department = context?.arguments?.["department"]; + if (department === "Engineering") { + return ["Alice", "Bob", "Charlie"].filter((n) => n.startsWith(value)); + } else if (department === "Sales") { + return ["David", "Eve", "Frank"].filter((n) => n.startsWith(value)); + } else if (department === "Marketing") { + return ["Grace", "Henry", "Iris"].filter((n) => n.startsWith(value)); + } else if (department === "Support") { + return ["John", "Kim", "Lee"].filter((n) => n.startsWith(value)); + } + return []; + } + ), }; + // Register the prompt server.registerPrompt( "completable-prompt", { title: "Team Management", - description: "Choose a team member to lead their specific department.", + description: "First argument choice narrows values for second argument.", argsSchema: promptArgsSchema, }, - async ({ department, name }) => ({ + ({ department, name }) => ({ messages: [ { role: "user", diff --git a/src/everything/prompts/index.ts b/src/everything/prompts/index.ts index 359c5a8e..6efa7b72 100644 --- a/src/everything/prompts/index.ts +++ b/src/everything/prompts/index.ts @@ -1,16 +1,17 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { registerSimplePrompt } from "./simple.js"; -import { registerComplexPrompt } from "./complex.js"; +import { registerArgumentsPrompt } from "./args.js"; import { registerPromptWithCompletions } from "./completions.js"; import { registerEmbeddedResourcePrompt } from "./resource.js"; /** * Register the prompts with the MCP server. + * * @param server */ export const registerPrompts = (server: McpServer) => { registerSimplePrompt(server); - registerComplexPrompt(server); + registerArgumentsPrompt(server); registerPromptWithCompletions(server); registerEmbeddedResourcePrompt(server); }; diff --git a/src/everything/prompts/resource.ts b/src/everything/prompts/resource.ts index 1003667d..82a06e56 100644 --- a/src/everything/prompts/resource.ts +++ b/src/everything/prompts/resource.ts @@ -1,15 +1,47 @@ import { z } from "zod"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { textResource, textResourceUri } from "../resources/template.js"; +import { completable } from "@modelcontextprotocol/sdk/server/completable.js"; +import { + textResource, + textResourceUri, + blobResourceUri, + blobResource, +} from "../resources/templates.js"; +/** + * Register a prompt with an embedded resource reference + * - Takes a resource type and id + * - Returns the corresponding dynamically created resource + * + * @param server + */ 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 + // Resource types + const BLOB_TYPE = "Blob"; + const TEXT_TYPE = "Text"; + const resourceTypes = [BLOB_TYPE, TEXT_TYPE]; + + // Prompt arguments const promptArgsSchema = { - resourceId: z.string().describe("ID of the text resource to fetch"), + 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] : []; + } + ), }; + // Register the prompt server.registerPrompt( "resource-prompt", { @@ -18,6 +50,15 @@ export const registerEmbeddedResourcePrompt = (server: McpServer) => { argsSchema: promptArgsSchema, }, (args) => { + // Validate resource type argument + const { resourceType } = args; + if (!resourceTypes.includes(resourceType)) { + throw new Error( + `Invalid resourceType: ${args?.resourceType}. Must be ${TEXT_TYPE} or ${BLOB_TYPE}.` + ); + } + + // Validate resourceId argument const resourceId = Number(args?.resourceId); if (!Number.isFinite(resourceId) || !Number.isInteger(resourceId)) { throw new Error( @@ -25,8 +66,15 @@ export const registerEmbeddedResourcePrompt = (server: McpServer) => { ); } - const uri = textResourceUri(resourceId); - const resource = textResource(uri, resourceId); + // Get resource based on the resource type + const uri = + resourceType === TEXT_TYPE + ? textResourceUri(resourceId) + : blobResourceUri(resourceId); + const resource = + resourceType === TEXT_TYPE + ? textResource(uri, resourceId) + : blobResource(uri, resourceId); return { messages: [ @@ -34,7 +82,7 @@ export const registerEmbeddedResourcePrompt = (server: McpServer) => { role: "user", content: { type: "text", - text: `This prompt includes the text resource with id: ${resourceId}. Please analyze the following resource:`, + text: `This prompt includes the ${resourceType} resource with id: ${resourceId}. Please analyze the following resource:`, }, }, { diff --git a/src/everything/prompts/simple.ts b/src/everything/prompts/simple.ts index abd15e4c..a2a0d2ee 100644 --- a/src/everything/prompts/simple.ts +++ b/src/everything/prompts/simple.ts @@ -1,6 +1,13 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +/** + * Register a simple prompt with no arguments + * - Returns the fixed text of the prompt with no modifications + * + * @param server + */ export const registerSimplePrompt = (server: McpServer) => { + // Register the prompt server.registerPrompt( "simple-prompt", { diff --git a/src/everything/resources/static.ts b/src/everything/resources/files.ts similarity index 69% rename from src/everything/resources/static.ts rename to src/everything/resources/files.ts index cc117d82..a5bf2166 100644 --- a/src/everything/resources/static.ts +++ b/src/everything/resources/files.ts @@ -3,30 +3,32 @@ import { dirname, join } from "path"; import { fileURLToPath } from "url"; import { readdirSync, readFileSync, statSync } from "fs"; -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); - /** - * Register static resources for each file in the docs folder. - * + * Register static file resources * - Each file in src/everything/docs is exposed as an individual static resource * - URIs follow the pattern: "demo://static/docs/" - * - Markdown files are served as text/markdown; others as text/plain + * - Markdown (.md) files are served as mime type "text/markdown" + * - Text (.txt) files are served as mime type "text/plain" + * - JSON (.json) files are served as mime type "application/json" * * @param server */ -export const registerStaticResources = (server: McpServer) => { +export const registerFileResources = (server: McpServer) => { + // Read the entries in the docs directory + const __filename = fileURLToPath(import.meta.url); + const __dirname = dirname(__filename); const docsDir = join(__dirname, "..", "docs"); - let entries: string[] = []; try { entries = readdirSync(docsDir); } catch (e) { - // If docs folder is missing or unreadable, just skip registration + // If docs/ folder is missing or unreadable, just skip registration return; } + // Register each file as a static resource for (const name of entries) { + // Only process files, not directories const fullPath = join(docsDir, name); try { const st = statSync(fullPath); @@ -35,11 +37,13 @@ export const registerStaticResources = (server: McpServer) => { continue; } + // Prepare file resource info const uri = `demo://resource/static/document/${encodeURIComponent(name)}`; const mimeType = getMimeType(name); const displayName = `Docs: ${name}`; const description = `Static document file exposed from /docs: ${name}`; + // Register file resource server.registerResource( displayName, uri, @@ -60,6 +64,10 @@ export const registerStaticResources = (server: McpServer) => { } }; +/** + * Get the mimetype based on filename + * @param fileName + */ function getMimeType(fileName: string): string { const lower = fileName.toLowerCase(); if (lower.endsWith(".md") || lower.endsWith(".markdown")) @@ -69,6 +77,10 @@ function getMimeType(fileName: string): string { return "text/plain"; } +/** + * Read a file or return an error message if it fails + * @param path + */ function readFileSafe(path: string): string { try { return readFileSync(path, "utf-8"); diff --git a/src/everything/resources/index.ts b/src/everything/resources/index.ts index 21033caa..c2db970f 100644 --- a/src/everything/resources/index.ts +++ b/src/everything/resources/index.ts @@ -1,6 +1,6 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { registerResourceTemplates } from "./template.js"; -import { registerStaticResources } from "./static.js"; +import { registerResourceTemplates } from "./templates.js"; +import { registerFileResources } from "./files.js"; /** * Register the resources with the MCP server. @@ -8,5 +8,5 @@ import { registerStaticResources } from "./static.js"; */ export const registerResources = (server: McpServer) => { registerResourceTemplates(server); - registerStaticResources(server); + registerFileResources(server); }; diff --git a/src/everything/resources/template.ts b/src/everything/resources/templates.ts similarity index 87% rename from src/everything/resources/template.ts rename to src/everything/resources/templates.ts index 570ef6a1..79279428 100644 --- a/src/everything/resources/template.ts +++ b/src/everything/resources/templates.ts @@ -11,6 +11,7 @@ const blobUriTemplate: string = `${blobUriBase}/{index}`; /** * Create a dynamic text resource + * - Exposed for use by embedded resource prompt example * @param uri * @param index */ @@ -25,6 +26,7 @@ export const textResource = (uri: URL, index: number) => { /** * Create a dynamic blob resource + * - Exposed for use by embedded resource prompt example * @param uri * @param index */ @@ -42,14 +44,22 @@ export const blobResource = (uri: URL, index: number) => { /** * Create a dynamic text resource URI + * - Exposed for use by embedded resource prompt example * @param index */ export const textResourceUri = (index: number) => new URL(`${textUriBase}/${index}`); +/** + * Create a dynamic blob resource URI + * - Exposed for use by embedded resource prompt example + * @param index + */ +export const blobResourceUri = (index: number) => + new URL(`${blobUriBase}/${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 * - List resources method will not return these resources @@ -63,6 +73,7 @@ export const textResourceUri = (index: number) => * @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 ( @@ -81,7 +92,7 @@ export const registerResourceTemplates = (server: McpServer) => { } }; - // Text resource template registration + // Register the text resource template server.registerResource( "Dynamic Text Resource", new ResourceTemplate(textUriTemplate, { list: undefined }), @@ -98,7 +109,7 @@ export const registerResourceTemplates = (server: McpServer) => { } ); - // Blob resource template registration + // Register the blob resource template server.registerResource( "Dynamic Blob Resource", new ResourceTemplate(blobUriTemplate, { list: undefined }),