From 4096313e095c714ec50048b6b20ce9c56853f361 Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Wed, 1 Jul 2026 12:13:09 +0300 Subject: [PATCH 1/4] initial @modelcontextprotocol/server & @modelcontextprotocol/node 2.0.0-alpha.3 --- package-lock.json | 193 +- src/everything/AGENTS.md | 66 +- src/everything/__tests__/prompts.test.ts | 276 ++- .../__tests__/registrations.test.ts | 255 +- src/everything/__tests__/resources.test.ts | 542 +++-- src/everything/__tests__/server.test.ts | 52 +- src/everything/__tests__/tools.test.ts | 2094 ++++++++--------- src/everything/docs/architecture.md | 6 +- src/everything/docs/extension.md | 12 +- src/everything/docs/features.md | 84 +- src/everything/docs/how-it-works.md | 34 +- src/everything/docs/instructions.md | 22 +- src/everything/docs/startup.md | 102 +- src/everything/docs/structure.md | 198 +- src/everything/index.ts | 62 +- src/everything/package.json | 95 +- src/everything/prompts/args.ts | 61 +- src/everything/prompts/completions.ts | 90 +- src/everything/prompts/index.ts | 18 +- src/everything/prompts/resource.ts | 136 +- src/everything/prompts/simple.ts | 36 +- src/everything/resources/files.ts | 110 +- src/everything/resources/index.ts | 36 +- src/everything/resources/session.ts | 80 +- src/everything/resources/subscriptions.ts | 172 +- src/everything/resources/templates.ts | 172 +- src/everything/server/index.ts | 159 +- src/everything/server/logging.ts | 102 +- src/everything/server/roots.ts | 112 +- src/everything/tools/echo.ts | 39 +- src/everything/tools/get-annotated-message.ts | 129 +- src/everything/tools/get-env.ts | 44 +- src/everything/tools/get-resource-links.ts | 100 +- .../tools/get-resource-reference.ts | 126 +- src/everything/tools/get-roots-list.ts | 128 +- .../tools/get-structured-content.ts | 111 +- src/everything/tools/get-sum.ts | 53 +- src/everything/tools/get-tiny-image.ts | 63 +- src/everything/tools/gzip-file-as-resource.ts | 326 ++- src/everything/tools/index.ts | 79 +- .../tools/simulate-research-query.ts | 345 --- .../tools/toggle-simulated-logging.ts | 64 +- .../tools/toggle-subscriber-updates.ts | 64 +- .../trigger-elicitation-request-async.ts | 449 ++-- .../tools/trigger-elicitation-request.ts | 380 ++- .../tools/trigger-long-running-operation.ts | 96 +- .../tools/trigger-sampling-request-async.ts | 384 ++- .../tools/trigger-sampling-request.ts | 123 +- .../tools/trigger-url-elicitation.ts | 296 ++- src/everything/transports/sse.ts | 105 +- src/everything/transports/stdio.ts | 32 +- src/everything/transports/streamableHttp.ts | 389 ++- src/everything/tsconfig.json | 12 +- src/everything/vitest.config.ts | 20 +- src/filesystem/__tests__/roots-utils.test.ts | 2 +- .../__tests__/structured-content.test.ts | 4 +- src/filesystem/index.ts | 157 +- src/filesystem/package.json | 5 +- src/filesystem/roots-utils.ts | 2 +- src/sequentialthinking/index.ts | 40 +- src/sequentialthinking/package.json | 4 +- 61 files changed, 4302 insertions(+), 5246 deletions(-) delete mode 100644 src/everything/tools/simulate-research-query.ts diff --git a/package-lock.json b/package-lock.json index 26261a7ade..8d89d89794 100644 --- a/package-lock.json +++ b/package-lock.json @@ -169,46 +169,68 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@modelcontextprotocol/sdk": { - "version": "1.29.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", - "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", + "node_modules/@modelcontextprotocol/client": { + "version": "2.0.0-alpha.3", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/client/-/client-2.0.0-alpha.3.tgz", + "integrity": "sha512-NNjaRRwHMKDgARgAlPt0Vkj963A+zoBOX2TtHptNjncjYCwadxx+reU5/H+nlimZmibhJEUznNMQr3UktzBVig==", "license": "MIT", "dependencies": { - "@hono/node-server": "^1.19.9", - "ajv": "^8.17.1", - "ajv-formats": "^3.0.1", - "content-type": "^1.0.5", - "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", - "express": "^5.2.1", - "express-rate-limit": "^8.2.1", - "hono": "^4.11.4", "jose": "^6.1.3", - "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", - "raw-body": "^3.0.0", - "zod": "^3.25 || ^4.0", - "zod-to-json-schema": "^3.25.1" + "zod": "^4.2.0" }, "engines": { - "node": ">=18" + "node": ">=20" + } + }, + "node_modules/@modelcontextprotocol/core": { + "version": "2.0.0-alpha.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/core/-/core-2.0.0-alpha.1.tgz", + "integrity": "sha512-AWdY7gcVq3mRdekXOI+Gh1W+B7aeWDaGERmcQ7PfSu9C0S78yu8jR+JseNWO375l6MM8FjIWVLD9U7KX2sbTpg==", + "license": "MIT", + "dependencies": { + "zod": "^4.2.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@modelcontextprotocol/node": { + "version": "2.0.0-alpha.3", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/node/-/node-2.0.0-alpha.3.tgz", + "integrity": "sha512-3S/1lsMoN4dLifs9cZXq5eZ0/yygjQY5iyx8dv9NKEM9r3347vSB3S025tIukr+Kc5y4uaq1ZsYg/idEycFe/Q==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9" + }, + "engines": { + "node": ">=20" }, "peerDependencies": { - "@cfworker/json-schema": "^4.1.1", - "zod": "^3.25 || ^4.0" + "@modelcontextprotocol/server": "^2.0.0-alpha.3", + "hono": "^4.11.4" }, "peerDependenciesMeta": { - "@cfworker/json-schema": { + "hono": { "optional": true - }, - "zod": { - "optional": false } } }, + "node_modules/@modelcontextprotocol/server": { + "version": "2.0.0-alpha.3", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/server/-/server-2.0.0-alpha.3.tgz", + "integrity": "sha512-PYdVDA6x3joCy/rw1mGdMSvqisPKlmt5QN/ILcT8R7efgvmhij14L87lJrWAm4pszaThzwpV49BmBBTZWtlY3A==", + "license": "MIT", + "dependencies": { + "zod": "^4.2.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/@modelcontextprotocol/server-everything": { "resolved": "src/everything", "link": true @@ -217,6 +239,32 @@ "resolved": "src/filesystem", "link": true }, + "node_modules/@modelcontextprotocol/server-legacy": { + "version": "2.0.0-alpha.3", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/server-legacy/-/server-legacy-2.0.0-alpha.3.tgz", + "integrity": "sha512-ny2TlhsXMlO5LguBB2udADMT1O96nFghKdh8P4BSkgh1AmQSq28PVrJaN3BVJQXlRv5PKRA4u9Ow0CNw3nfXKA==", + "deprecated": "This package is a frozen copy of v1's SSE transport and OAuth Authorization Server helpers for migration purposes only. Use StreamableHTTP from @modelcontextprotocol/server and a dedicated OAuth server in production. Will not receive new features.", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "cors": "^2.8.5", + "express-rate-limit": "^8.2.1", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^4.2.0" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "express": "^4.18.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "express": { + "optional": true + } + } + }, "node_modules/@modelcontextprotocol/server-memory": { "resolved": "src/memory", "link": true @@ -867,39 +915,6 @@ "node": ">= 0.6" } }, - "node_modules/ajv": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", - "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", - "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", - "license": "MIT", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, "node_modules/ansi-regex": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", @@ -1496,28 +1511,6 @@ "express": ">= 4.11" } }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "license": "MIT" - }, - "node_modules/fast-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", - "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" - }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -1770,6 +1763,7 @@ "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.25.tgz", "integrity": "sha512-2NFaIyNVgJmBs/ecmtGzlmluTFs5cHEWGTdu0t1HBwYzoGXOL5nUQBRMXsXWla5i4KkG//QMzVP88m1+I3fdAQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=16.9.0" } @@ -1982,18 +1976,6 @@ "dev": true, "license": "MIT" }, - "node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" - }, - "node_modules/json-schema-typed": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", - "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", - "license": "BSD-2-Clause" - }, "node_modules/jszip": { "version": "3.10.1", "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", @@ -2747,15 +2729,6 @@ "node": ">=0.10.0" } }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/resolve": { "version": "1.22.12", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", @@ -3794,21 +3767,15 @@ "url": "https://github.com/sponsors/colinhacks" } }, - "node_modules/zod-to-json-schema": { - "version": "3.25.2", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", - "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", - "license": "ISC", - "peerDependencies": { - "zod": "^3.25.28 || ^4" - } - }, "src/everything": { "name": "@modelcontextprotocol/server-everything", "version": "2.0.0", "license": "SEE LICENSE IN LICENSE", "dependencies": { - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/core": "^2.0.0-alpha.1", + "@modelcontextprotocol/node": "^2.0.0-alpha.3", + "@modelcontextprotocol/server": "^2.0.0-alpha.3", + "@modelcontextprotocol/server-legacy": "^2.0.0-alpha.3", "cors": "^2.8.5", "express": "^5.2.1", "jszip": "^3.10.1", @@ -3832,7 +3799,8 @@ "version": "0.6.3", "license": "SEE LICENSE IN LICENSE", "dependencies": { - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/client": "^2.0.0-alpha.3", + "@modelcontextprotocol/server": "^2.0.0-alpha.3", "diff": "^8.0.3", "glob": "^10.5.0", "minimatch": "^10.0.1" @@ -3855,7 +3823,8 @@ "version": "0.6.3", "license": "SEE LICENSE IN LICENSE", "dependencies": { - "@modelcontextprotocol/sdk": "^1.29.0" + "@modelcontextprotocol/core": "^2.0.0-alpha.1", + "@modelcontextprotocol/server": "^2.0.0-alpha.3" }, "bin": { "mcp-server-memory": "dist/index.js" @@ -3873,7 +3842,7 @@ "version": "0.6.2", "license": "SEE LICENSE IN LICENSE", "dependencies": { - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/server": "^2.0.0-alpha.3", "chalk": "^5.3.0", "yargs": "^17.7.2" }, diff --git a/src/everything/AGENTS.md b/src/everything/AGENTS.md index c4a6df1348..85ec40cf7d 100644 --- a/src/everything/AGENTS.md +++ b/src/everything/AGENTS.md @@ -2,30 +2,30 @@ ## Build, Test & Run Commands -- Build: `npm run build` - Compiles TypeScript to JavaScript -- Watch mode: `npm run watch` - Watches for changes and rebuilds automatically -- Run STDIO server: `npm run start:stdio` - Starts the MCP server using stdio transport -- Run SSE server: `npm run start:sse` - Starts the MCP server with SSE transport -- Run StreamableHttp server: `npm run start:streamableHttp` - Starts the MCP server with StreamableHttp transport -- Prepare release: `npm run prepare` - Builds the project for publishing +- Build: `npm run build` - Compiles TypeScript to JavaScript +- Watch mode: `npm run watch` - Watches for changes and rebuilds automatically +- Run STDIO server: `npm run start:stdio` - Starts the MCP server using stdio transport +- Run SSE server: `npm run start:sse` - Starts the MCP server with SSE transport +- Run StreamableHttp server: `npm run start:streamableHttp` - Starts the MCP server with StreamableHttp transport +- Prepare release: `npm run prepare` - Builds the project for publishing ## Code Style Guidelines -- Use ES modules with `.js` extension in import paths -- Strictly type all functions and variables with TypeScript -- Follow zod schema patterns for tool input validation -- Prefer async/await over callbacks and Promise chains -- Place all imports at top of file, grouped by external then internal -- Use descriptive variable names that clearly indicate purpose -- Implement proper cleanup for timers and resources in server shutdown -- Handle errors with try/catch blocks and provide clear error messages -- Use consistent indentation (2 spaces) and trailing commas in multi-line objects -- Match existing code style, import order, and module layout in the respective folder. -- Use camelCase for variables/functions, -- Use PascalCase for types/classes, -- Use UPPER_CASE for constants -- Use kebab-case for file names and registered tools, prompts, and resources. -- Use verbs for tool names, e.g., `get-annotated-message` instead of `annotated-message` +- Use ES modules with `.js` extension in import paths +- Strictly type all functions and variables with TypeScript +- Follow zod schema patterns for tool input validation +- Prefer async/await over callbacks and Promise chains +- Place all imports at top of file, grouped by external then internal +- Use descriptive variable names that clearly indicate purpose +- Implement proper cleanup for timers and resources in server shutdown +- Handle errors with try/catch blocks and provide clear error messages +- Use consistent indentation (2 spaces) and trailing commas in multi-line objects +- Match existing code style, import order, and module layout in the respective folder. +- Use camelCase for variables/functions, +- Use PascalCase for types/classes, +- Use UPPER_CASE for constants +- Use kebab-case for file names and registered tools, prompts, and resources. +- Use verbs for tool names, e.g., `get-annotated-message` instead of `annotated-message` ## Extending the Server @@ -35,18 +35,18 @@ The server factory is `src/everything/server/index.ts` and registers all feature ### High-level -- Tools live under `src/everything/tools/` and are registered via `registerTools(server)`. -- Resources live under `src/everything/resources/` and are registered via `registerResources(server)`. -- Prompts live under `src/everything/prompts/` and are registered via `registerPrompts(server)`. -- Subscriptions and simulated update routines are under `src/everything/resources/subscriptions.ts`. -- Logging helpers are under `src/everything/server/logging.ts`. -- Transport managers are under `src/everything/transports/`. +- Tools live under `src/everything/tools/` and are registered via `registerTools(server)`. +- Resources live under `src/everything/resources/` and are registered via `registerResources(server)`. +- Prompts live under `src/everything/prompts/` and are registered via `registerPrompts(server)`. +- Subscriptions and simulated update routines are under `src/everything/resources/subscriptions.ts`. +- Logging helpers are under `src/everything/server/logging.ts`. +- Transport managers are under `src/everything/transports/`. ### When adding a new feature -- Follow the existing file/module pattern in its folder (naming, exports, and registration function). -- Export a `registerX(server)` function that registers new items with the MCP SDK in the same style as existing ones. -- Wire your new module into the central index (e.g., update `tools/index.ts`, `resources/index.ts`, or `prompts/index.ts`). -- Ensure schemas (for tools) are accurate JSON Schema and include helpful descriptions and examples. - `server/index.ts` and usages in `logging.ts` and `subscriptions.ts`. -- Keep the docs in `src/everything/docs/` up to date if you add or modify noteworthy features. +- Follow the existing file/module pattern in its folder (naming, exports, and registration function). +- Export a `registerX(server)` function that registers new items with the MCP SDK in the same style as existing ones. +- Wire your new module into the central index (e.g., update `tools/index.ts`, `resources/index.ts`, or `prompts/index.ts`). +- Ensure schemas (for tools) are accurate JSON Schema and include helpful descriptions and examples. + `server/index.ts` and usages in `logging.ts` and `subscriptions.ts`. +- Keep the docs in `src/everything/docs/` up to date if you add or modify noteworthy features. diff --git a/src/everything/__tests__/prompts.test.ts b/src/everything/__tests__/prompts.test.ts index bff176ac47..20b6728208 100644 --- a/src/everything/__tests__/prompts.test.ts +++ b/src/everything/__tests__/prompts.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect, vi } from 'vitest'; -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { McpServer } from '@modelcontextprotocol/server'; import { registerSimplePrompt } from '../prompts/simple.js'; import { registerArgumentsPrompt } from '../prompts/args.js'; import { registerPromptWithCompletions } from '../prompts/completions.js'; @@ -7,173 +7,161 @@ import { registerEmbeddedResourcePrompt } from '../prompts/resource.js'; // Helper to capture registered prompt handlers function createMockServer() { - const handlers: Map = new Map(); - const configs: Map = new Map(); + const handlers: Map = new Map(); + const configs: Map = new Map(); - const mockServer = { - registerPrompt: vi.fn((name: string, config: any, handler: Function) => { - handlers.set(name, handler); - configs.set(name, config); - }), - } as unknown as McpServer; + const mockServer = { + registerPrompt: vi.fn((name: string, config: any, handler: Function) => { + handlers.set(name, handler); + configs.set(name, config); + }) + } as unknown as McpServer; - return { mockServer, handlers, configs }; + return { mockServer, handlers, configs }; } describe('Prompts', () => { - describe('simple-prompt', () => { - it('should return fixed message with no arguments', () => { - const { mockServer, handlers } = createMockServer(); - registerSimplePrompt(mockServer); - - const handler = handlers.get('simple-prompt')!; - const result = handler(); - - expect(result).toEqual({ - messages: [ - { - role: 'user', - content: { - type: 'text', - text: 'This is a simple prompt without arguments.', - }, - }, - ], - }); + describe('simple-prompt', () => { + it('should return fixed message with no arguments', () => { + const { mockServer, handlers } = createMockServer(); + registerSimplePrompt(mockServer); + + const handler = handlers.get('simple-prompt')!; + const result = handler(); + + expect(result).toEqual({ + messages: [ + { + role: 'user', + content: { + type: 'text', + text: 'This is a simple prompt without arguments.' + } + } + ] + }); + }); }); - }); - describe('args-prompt', () => { - it('should include city in message', () => { - const { mockServer, handlers } = createMockServer(); - registerArgumentsPrompt(mockServer); + describe('args-prompt', () => { + it('should include city in message', () => { + const { mockServer, handlers } = createMockServer(); + registerArgumentsPrompt(mockServer); - const handler = handlers.get('args-prompt')!; - const result = handler({ city: 'San Francisco' }); + const handler = handlers.get('args-prompt')!; + const result = handler({ city: 'San Francisco' }); - expect(result.messages[0].content.text).toBe("What's weather in San Francisco?"); - }); - - it('should include city and state in message', () => { - const { mockServer, handlers } = createMockServer(); - registerArgumentsPrompt(mockServer); - - const handler = handlers.get('args-prompt')!; - const result = handler({ city: 'San Francisco', state: 'California' }); - - expect(result.messages[0].content.text).toBe( - "What's weather in San Francisco, California?" - ); - }); - - it('should handle city only (optional state omitted)', () => { - const { mockServer, handlers } = createMockServer(); - registerArgumentsPrompt(mockServer); - - const handler = handlers.get('args-prompt')!; - const result = handler({ city: 'New York' }); - - expect(result.messages[0].content.text).toBe("What's weather in New York?"); - expect(result.messages[0].content.text).not.toContain(','); - expect(result.messages[0].role).toBe('user'); - expect(result.messages[0].content.type).toBe('text'); - }); - }); + expect(result.messages[0].content.text).toBe("What's weather in San Francisco?"); + }); - describe('completable-prompt', () => { - it('should generate promotion message with department and name', () => { - const { mockServer, handlers } = createMockServer(); - registerPromptWithCompletions(mockServer); + it('should include city and state in message', () => { + const { mockServer, handlers } = createMockServer(); + registerArgumentsPrompt(mockServer); - const handler = handlers.get('completable-prompt')!; - const result = handler({ department: 'Engineering', name: 'Alice' }); + const handler = handlers.get('args-prompt')!; + const result = handler({ city: 'San Francisco', state: 'California' }); - expect(result.messages[0].content.text).toBe( - 'Please promote Alice to the head of the Engineering team.' - ); - }); - - it('should work with different departments', () => { - const { mockServer, handlers } = createMockServer(); - registerPromptWithCompletions(mockServer); + expect(result.messages[0].content.text).toBe("What's weather in San Francisco, California?"); + }); - const handler = handlers.get('completable-prompt')!; + it('should handle city only (optional state omitted)', () => { + const { mockServer, handlers } = createMockServer(); + registerArgumentsPrompt(mockServer); - const salesResult = handler({ department: 'Sales', name: 'David' }); - expect(salesResult.messages[0].content.text).toContain('Sales'); - expect(salesResult.messages[0].content.text).toContain('David'); - expect(salesResult.messages[0].role).toBe('user'); + const handler = handlers.get('args-prompt')!; + const result = handler({ city: 'New York' }); - const marketingResult = handler({ department: 'Marketing', name: 'Grace' }); - expect(marketingResult.messages[0].content.text).toContain('Marketing'); - expect(marketingResult.messages[0].content.text).toContain('Grace'); + expect(result.messages[0].content.text).toBe("What's weather in New York?"); + expect(result.messages[0].content.text).not.toContain(','); + expect(result.messages[0].role).toBe('user'); + expect(result.messages[0].content.type).toBe('text'); + }); }); - }); - describe('resource-prompt', () => { - it('should return text resource reference', () => { - const { mockServer, handlers } = createMockServer(); - registerEmbeddedResourcePrompt(mockServer); + describe('completable-prompt', () => { + it('should generate promotion message with department and name', () => { + const { mockServer, handlers } = createMockServer(); + registerPromptWithCompletions(mockServer); - const handler = handlers.get('resource-prompt')!; - const result = handler({ resourceType: 'Text', resourceId: '1' }); + const handler = handlers.get('completable-prompt')!; + const result = handler({ department: 'Engineering', name: 'Alice' }); - expect(result.messages).toHaveLength(2); - expect(result.messages[0].content.text).toContain('Text'); - expect(result.messages[0].content.text).toContain('1'); - expect(result.messages[1].content.type).toBe('resource'); - expect(result.messages[1].content.resource.uri).toContain('text/1'); - }); + expect(result.messages[0].content.text).toBe('Please promote Alice to the head of the Engineering team.'); + }); - it('should return blob resource reference', () => { - const { mockServer, handlers } = createMockServer(); - registerEmbeddedResourcePrompt(mockServer); + it('should work with different departments', () => { + const { mockServer, handlers } = createMockServer(); + registerPromptWithCompletions(mockServer); - const handler = handlers.get('resource-prompt')!; - const result = handler({ resourceType: 'Blob', resourceId: '5' }); - - expect(result.messages[0].content.text).toContain('Blob'); - expect(result.messages[1].content.resource.uri).toContain('blob/5'); - }); + const handler = handlers.get('completable-prompt')!; - it('should reject invalid resource type', () => { - const { mockServer, handlers } = createMockServer(); - registerEmbeddedResourcePrompt(mockServer); + const salesResult = handler({ department: 'Sales', name: 'David' }); + expect(salesResult.messages[0].content.text).toContain('Sales'); + expect(salesResult.messages[0].content.text).toContain('David'); + expect(salesResult.messages[0].role).toBe('user'); - const handler = handlers.get('resource-prompt')!; - expect(() => handler({ resourceType: 'Invalid', resourceId: '1' })).toThrow( - 'Invalid resourceType' - ); + const marketingResult = handler({ department: 'Marketing', name: 'Grace' }); + expect(marketingResult.messages[0].content.text).toContain('Marketing'); + expect(marketingResult.messages[0].content.text).toContain('Grace'); + }); }); - it('should reject invalid resource ID', () => { - const { mockServer, handlers } = createMockServer(); - registerEmbeddedResourcePrompt(mockServer); - - const handler = handlers.get('resource-prompt')!; - expect(() => handler({ resourceType: 'Text', resourceId: '-1' })).toThrow( - 'Invalid resourceId' - ); - expect(() => handler({ resourceType: 'Text', resourceId: '0' })).toThrow( - 'Invalid resourceId' - ); - expect(() => handler({ resourceType: 'Text', resourceId: 'abc' })).toThrow( - 'Invalid resourceId' - ); - }); - - it('should include both intro text and resource messages', () => { - const { mockServer, handlers } = createMockServer(); - registerEmbeddedResourcePrompt(mockServer); - - const handler = handlers.get('resource-prompt')!; - const result = handler({ resourceType: 'Text', resourceId: '3' }); - - expect(result.messages).toHaveLength(2); - expect(result.messages[0].role).toBe('user'); - expect(result.messages[0].content.type).toBe('text'); - expect(result.messages[1].role).toBe('user'); - expect(result.messages[1].content.type).toBe('resource'); + describe('resource-prompt', () => { + it('should return text resource reference', () => { + const { mockServer, handlers } = createMockServer(); + registerEmbeddedResourcePrompt(mockServer); + + const handler = handlers.get('resource-prompt')!; + const result = handler({ resourceType: 'Text', resourceId: '1' }); + + expect(result.messages).toHaveLength(2); + expect(result.messages[0].content.text).toContain('Text'); + expect(result.messages[0].content.text).toContain('1'); + expect(result.messages[1].content.type).toBe('resource'); + expect(result.messages[1].content.resource.uri).toContain('text/1'); + }); + + it('should return blob resource reference', () => { + const { mockServer, handlers } = createMockServer(); + registerEmbeddedResourcePrompt(mockServer); + + const handler = handlers.get('resource-prompt')!; + const result = handler({ resourceType: 'Blob', resourceId: '5' }); + + expect(result.messages[0].content.text).toContain('Blob'); + expect(result.messages[1].content.resource.uri).toContain('blob/5'); + }); + + it('should reject invalid resource type', () => { + const { mockServer, handlers } = createMockServer(); + registerEmbeddedResourcePrompt(mockServer); + + const handler = handlers.get('resource-prompt')!; + expect(() => handler({ resourceType: 'Invalid', resourceId: '1' })).toThrow('Invalid resourceType'); + }); + + it('should reject invalid resource ID', () => { + const { mockServer, handlers } = createMockServer(); + registerEmbeddedResourcePrompt(mockServer); + + const handler = handlers.get('resource-prompt')!; + expect(() => handler({ resourceType: 'Text', resourceId: '-1' })).toThrow('Invalid resourceId'); + expect(() => handler({ resourceType: 'Text', resourceId: '0' })).toThrow('Invalid resourceId'); + expect(() => handler({ resourceType: 'Text', resourceId: 'abc' })).toThrow('Invalid resourceId'); + }); + + it('should include both intro text and resource messages', () => { + const { mockServer, handlers } = createMockServer(); + registerEmbeddedResourcePrompt(mockServer); + + const handler = handlers.get('resource-prompt')!; + const result = handler({ resourceType: 'Text', resourceId: '3' }); + + expect(result.messages).toHaveLength(2); + expect(result.messages[0].role).toBe('user'); + expect(result.messages[0].content.type).toBe('text'); + expect(result.messages[1].role).toBe('user'); + expect(result.messages[1].content.type).toBe('resource'); + }); }); - }); }); diff --git a/src/everything/__tests__/registrations.test.ts b/src/everything/__tests__/registrations.test.ts index 421d759e07..c87973db20 100644 --- a/src/everything/__tests__/registrations.test.ts +++ b/src/everything/__tests__/registrations.test.ts @@ -1,155 +1,144 @@ import { describe, it, expect, vi } from 'vitest'; -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { McpServer } from '@modelcontextprotocol/server'; // Create mock server function createMockServer() { - return { - registerTool: vi.fn(), - registerPrompt: vi.fn(), - registerResource: vi.fn(), - server: { - getClientCapabilities: vi.fn(() => ({})), - setRequestHandler: vi.fn(), - }, - sendLoggingMessage: vi.fn(), - sendResourceUpdated: vi.fn(), - } as unknown as McpServer; -} - -describe('Registration Index Files', () => { - describe('tools/index.ts', () => { - it('should register all standard tools', async () => { - const { registerTools } = await import('../tools/index.js'); - const mockServer = createMockServer(); - - registerTools(mockServer); - - // Should register 12 standard tools (non-conditional) - expect(mockServer.registerTool).toHaveBeenCalledTimes(12); - - // Verify specific tools are registered - const registeredTools = (mockServer.registerTool as any).mock.calls.map( - (call: any[]) => call[0] - ); - expect(registeredTools).toContain('echo'); - expect(registeredTools).toContain('get-sum'); - expect(registeredTools).toContain('get-env'); - expect(registeredTools).toContain('get-tiny-image'); - expect(registeredTools).toContain('get-structured-content'); - expect(registeredTools).toContain('get-annotated-message'); - expect(registeredTools).toContain('trigger-long-running-operation'); - expect(registeredTools).toContain('get-resource-links'); - expect(registeredTools).toContain('get-resource-reference'); - expect(registeredTools).toContain('gzip-file-as-resource'); - expect(registeredTools).toContain('toggle-simulated-logging'); - expect(registeredTools).toContain('toggle-subscriber-updates'); - }); - - it('should register conditional tools based on capabilities', async () => { - const { registerConditionalTools } = await import('../tools/index.js'); - - // Server with all capabilities including experimental tasks API - const mockServerWithCapabilities = { + return { registerTool: vi.fn(), + registerPrompt: vi.fn(), + registerResource: vi.fn(), server: { - getClientCapabilities: vi.fn(() => ({ - roots: {}, - elicitation: { url: {} }, - sampling: {}, - })), - }, - experimental: { - tasks: { - registerToolTask: vi.fn(), - }, + getClientCapabilities: vi.fn(() => ({})), + setRequestHandler: vi.fn() }, - } as unknown as McpServer; - - registerConditionalTools(mockServerWithCapabilities); - - // Should register 4 conditional tools via registerTool when all capabilities - // are present. Task-based tools register via registerToolTask (counted separately), - // so they are not included in this registerTool count. - expect(mockServerWithCapabilities.registerTool).toHaveBeenCalledTimes(4); - - const registeredTools = ( - mockServerWithCapabilities.registerTool as any - ).mock.calls.map((call: any[]) => call[0]); - expect(registeredTools).toContain('get-roots-list'); - expect(registeredTools).toContain('trigger-elicitation-request'); - expect(registeredTools).toContain('trigger-url-elicitation'); - expect(registeredTools).toContain('trigger-sampling-request'); + sendLoggingMessage: vi.fn(), + sendResourceUpdated: vi.fn() + } as unknown as McpServer; +} - // Task-based tools are registered via experimental.tasks.registerToolTask - expect(mockServerWithCapabilities.experimental.tasks.registerToolTask).toHaveBeenCalled(); +describe('Registration Index Files', () => { + describe('tools/index.ts', () => { + it('should register all standard tools', async () => { + const { registerTools } = await import('../tools/index.js'); + const mockServer = createMockServer(); + + registerTools(mockServer); + + // Should register 12 standard tools (non-conditional) + expect(mockServer.registerTool).toHaveBeenCalledTimes(12); + + // Verify specific tools are registered + const registeredTools = (mockServer.registerTool as any).mock.calls.map((call: any[]) => call[0]); + expect(registeredTools).toContain('echo'); + expect(registeredTools).toContain('get-sum'); + expect(registeredTools).toContain('get-env'); + expect(registeredTools).toContain('get-tiny-image'); + expect(registeredTools).toContain('get-structured-content'); + expect(registeredTools).toContain('get-annotated-message'); + expect(registeredTools).toContain('trigger-long-running-operation'); + expect(registeredTools).toContain('get-resource-links'); + expect(registeredTools).toContain('get-resource-reference'); + expect(registeredTools).toContain('gzip-file-as-resource'); + expect(registeredTools).toContain('toggle-simulated-logging'); + expect(registeredTools).toContain('toggle-subscriber-updates'); + }); + + it('should register conditional tools based on capabilities', async () => { + const { registerConditionalTools } = await import('../tools/index.js'); + + // Server with all capabilities including experimental tasks API + const mockServerWithCapabilities = { + registerTool: vi.fn(), + server: { + getClientCapabilities: vi.fn(() => ({ + roots: {}, + elicitation: { url: {} }, + sampling: {} + })) + }, + experimental: { + tasks: { + registerToolTask: vi.fn() + } + } + } as unknown as McpServer; + + registerConditionalTools(mockServerWithCapabilities); + + // Should register 4 conditional tools via registerTool when all capabilities + // are present. Task-based tools register via registerToolTask (counted separately), + // so they are not included in this registerTool count. + expect(mockServerWithCapabilities.registerTool).toHaveBeenCalledTimes(4); + + const registeredTools = (mockServerWithCapabilities.registerTool as any).mock.calls.map((call: any[]) => call[0]); + expect(registeredTools).toContain('get-roots-list'); + expect(registeredTools).toContain('trigger-elicitation-request'); + expect(registeredTools).toContain('trigger-url-elicitation'); + expect(registeredTools).toContain('trigger-sampling-request'); + }); + + it('should not register conditional tools when capabilities missing', async () => { + const { registerConditionalTools } = await import('../tools/index.js'); + + const mockServerNoCapabilities = { + registerTool: vi.fn(), + server: { + getClientCapabilities: vi.fn(() => ({})) + }, + experimental: { + tasks: { + registerToolTask: vi.fn() + } + } + } as unknown as McpServer; + + registerConditionalTools(mockServerNoCapabilities); + + // Should not register any capability-gated tools when capabilities are missing + expect(mockServerNoCapabilities.registerTool).not.toHaveBeenCalled(); + }); }); - it('should not register conditional tools when capabilities missing', async () => { - const { registerConditionalTools } = await import('../tools/index.js'); + describe('prompts/index.ts', () => { + it('should register all prompts', async () => { + const { registerPrompts } = await import('../prompts/index.js'); + const mockServer = createMockServer(); - const mockServerNoCapabilities = { - registerTool: vi.fn(), - server: { - getClientCapabilities: vi.fn(() => ({})), - }, - experimental: { - tasks: { - registerToolTask: vi.fn(), - }, - }, - } as unknown as McpServer; + registerPrompts(mockServer); - registerConditionalTools(mockServerNoCapabilities); + // Should register 4 prompts + expect(mockServer.registerPrompt).toHaveBeenCalledTimes(4); - // Should not register any capability-gated tools when capabilities are missing - expect(mockServerNoCapabilities.registerTool).not.toHaveBeenCalled(); + const registeredPrompts = (mockServer.registerPrompt as any).mock.calls.map((call: any[]) => call[0]); + expect(registeredPrompts).toContain('simple-prompt'); + expect(registeredPrompts).toContain('args-prompt'); + expect(registeredPrompts).toContain('completable-prompt'); + expect(registeredPrompts).toContain('resource-prompt'); + }); }); - }); - - describe('prompts/index.ts', () => { - it('should register all prompts', async () => { - const { registerPrompts } = await import('../prompts/index.js'); - const mockServer = createMockServer(); - registerPrompts(mockServer); + describe('resources/index.ts', () => { + it('should register resource templates', async () => { + const { registerResources } = await import('../resources/index.js'); + const mockServer = createMockServer(); - // Should register 4 prompts - expect(mockServer.registerPrompt).toHaveBeenCalledTimes(4); + registerResources(mockServer); - const registeredPrompts = (mockServer.registerPrompt as any).mock.calls.map( - (call: any[]) => call[0] - ); - expect(registeredPrompts).toContain('simple-prompt'); - expect(registeredPrompts).toContain('args-prompt'); - expect(registeredPrompts).toContain('completable-prompt'); - expect(registeredPrompts).toContain('resource-prompt'); - }); - }); - - describe('resources/index.ts', () => { - it('should register resource templates', async () => { - const { registerResources } = await import('../resources/index.js'); - const mockServer = createMockServer(); - - registerResources(mockServer); - - // Should register at least the 2 resource templates (text and blob) plus file resources - expect(mockServer.registerResource).toHaveBeenCalled(); - const registeredResources = (mockServer.registerResource as any).mock.calls.map( - (call: any[]) => call[0] - ); - expect(registeredResources).toContain('Dynamic Text Resource'); - expect(registeredResources).toContain('Dynamic Blob Resource'); - }); + // Should register at least the 2 resource templates (text and blob) plus file resources + expect(mockServer.registerResource).toHaveBeenCalled(); + const registeredResources = (mockServer.registerResource as any).mock.calls.map((call: any[]) => call[0]); + expect(registeredResources).toContain('Dynamic Text Resource'); + expect(registeredResources).toContain('Dynamic Blob Resource'); + }); - it('should read instructions from file', async () => { - const { readInstructions } = await import('../resources/index.js'); + it('should read instructions from file', async () => { + const { readInstructions } = await import('../resources/index.js'); - const instructions = readInstructions(); + const instructions = readInstructions(); - // Should return a string (either content or error message) - expect(typeof instructions).toBe('string'); - expect(instructions.length).toBeGreaterThan(0); + // Should return a string (either content or error message) + expect(typeof instructions).toBe('string'); + expect(instructions.length).toBeGreaterThan(0); + }); }); - }); }); diff --git a/src/everything/__tests__/resources.test.ts b/src/everything/__tests__/resources.test.ts index a22b904175..b48f7e1607 100644 --- a/src/everything/__tests__/resources.test.ts +++ b/src/everything/__tests__/resources.test.ts @@ -1,327 +1,309 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { McpServer, ResourceTemplate } from '@modelcontextprotocol/server'; import { - textResource, - blobResource, - textResourceUri, - blobResourceUri, - RESOURCE_TYPE_TEXT, - RESOURCE_TYPE_BLOB, - RESOURCE_TYPES, - resourceTypeCompleter, - resourceIdForPromptCompleter, - resourceIdForResourceTemplateCompleter, - registerResourceTemplates, + textResource, + blobResource, + textResourceUri, + blobResourceUri, + RESOURCE_TYPE_TEXT, + RESOURCE_TYPE_BLOB, + RESOURCE_TYPES, + resourceTypeCompleter, + resourceIdForPromptCompleter, + resourceIdForResourceTemplateCompleter, + registerResourceTemplates } from '../resources/templates.js'; -import { - getSessionResourceURI, - registerSessionResource, -} from '../resources/session.js'; +import { getSessionResourceURI, registerSessionResource } from '../resources/session.js'; import { registerFileResources } from '../resources/files.js'; -import { - setSubscriptionHandlers, - beginSimulatedResourceUpdates, - stopSimulatedResourceUpdates, -} from '../resources/subscriptions.js'; +import { setSubscriptionHandlers, beginSimulatedResourceUpdates, stopSimulatedResourceUpdates } from '../resources/subscriptions.js'; describe('Resource Templates', () => { - describe('Constants', () => { - it('should include both types in RESOURCE_TYPES array', () => { - expect(RESOURCE_TYPES).toContain(RESOURCE_TYPE_TEXT); - expect(RESOURCE_TYPES).toContain(RESOURCE_TYPE_BLOB); - expect(RESOURCE_TYPES).toHaveLength(2); + describe('Constants', () => { + it('should include both types in RESOURCE_TYPES array', () => { + expect(RESOURCE_TYPES).toContain(RESOURCE_TYPE_TEXT); + expect(RESOURCE_TYPES).toContain(RESOURCE_TYPE_BLOB); + expect(RESOURCE_TYPES).toHaveLength(2); + }); }); - }); - describe('textResourceUri', () => { - it('should create URL for text resource', () => { - const uri = textResourceUri(1); - expect(uri.toString()).toBe('demo://resource/dynamic/text/1'); - }); + describe('textResourceUri', () => { + it('should create URL for text resource', () => { + const uri = textResourceUri(1); + expect(uri.toString()).toBe('demo://resource/dynamic/text/1'); + }); - it('should handle different resource IDs', () => { - expect(textResourceUri(5).toString()).toBe('demo://resource/dynamic/text/5'); - expect(textResourceUri(100).toString()).toBe('demo://resource/dynamic/text/100'); + it('should handle different resource IDs', () => { + expect(textResourceUri(5).toString()).toBe('demo://resource/dynamic/text/5'); + expect(textResourceUri(100).toString()).toBe('demo://resource/dynamic/text/100'); + }); }); - }); - describe('blobResourceUri', () => { - it('should create URL for blob resource', () => { - const uri = blobResourceUri(1); - expect(uri.toString()).toBe('demo://resource/dynamic/blob/1'); - }); + describe('blobResourceUri', () => { + it('should create URL for blob resource', () => { + const uri = blobResourceUri(1); + expect(uri.toString()).toBe('demo://resource/dynamic/blob/1'); + }); - it('should handle different resource IDs', () => { - expect(blobResourceUri(5).toString()).toBe('demo://resource/dynamic/blob/5'); - expect(blobResourceUri(100).toString()).toBe('demo://resource/dynamic/blob/100'); + it('should handle different resource IDs', () => { + expect(blobResourceUri(5).toString()).toBe('demo://resource/dynamic/blob/5'); + expect(blobResourceUri(100).toString()).toBe('demo://resource/dynamic/blob/100'); + }); }); - }); - describe('textResource', () => { - it('should create text resource with correct structure', () => { - const uri = textResourceUri(1); - const resource = textResource(uri, 1); + describe('textResource', () => { + it('should create text resource with correct structure', () => { + const uri = textResourceUri(1); + const resource = textResource(uri, 1); - expect(resource.uri).toBe(uri.toString()); - expect(resource.mimeType).toBe('text/plain'); - expect(resource.text).toContain('Resource 1'); - expect(resource.text).toContain('plaintext'); - }); + expect(resource.uri).toBe(uri.toString()); + expect(resource.mimeType).toBe('text/plain'); + expect(resource.text).toContain('Resource 1'); + expect(resource.text).toContain('plaintext'); + }); - it('should include timestamp in content', () => { - const uri = textResourceUri(2); - const resource = textResource(uri, 2); + it('should include timestamp in content', () => { + const uri = textResourceUri(2); + const resource = textResource(uri, 2); - // Timestamp format varies, just check it contains time-related content - expect(resource.text).toMatch(/\d/); + // Timestamp format varies, just check it contains time-related content + expect(resource.text).toMatch(/\d/); + }); }); - }); - describe('blobResource', () => { - it('should create blob resource with correct structure', () => { - const uri = blobResourceUri(1); - const resource = blobResource(uri, 1); - - expect(resource.uri).toBe(uri.toString()); - expect(resource.mimeType).toBe('text/plain'); - expect(resource.blob).toBeDefined(); + describe('blobResource', () => { + it('should create blob resource with correct structure', () => { + const uri = blobResourceUri(1); + const resource = blobResource(uri, 1); + + expect(resource.uri).toBe(uri.toString()); + expect(resource.mimeType).toBe('text/plain'); + expect(resource.blob).toBeDefined(); + }); + + it('should create valid base64 encoded content', () => { + const uri = blobResourceUri(3); + const resource = blobResource(uri, 3); + + // Decode and verify content + const decoded = Buffer.from(resource.blob, 'base64').toString(); + expect(decoded).toContain('Resource 3'); + expect(decoded).toContain('base64 blob'); + }); }); - it('should create valid base64 encoded content', () => { - const uri = blobResourceUri(3); - const resource = blobResource(uri, 3); - - // Decode and verify content - const decoded = Buffer.from(resource.blob, 'base64').toString(); - expect(decoded).toContain('Resource 3'); - expect(decoded).toContain('base64 blob'); - }); - }); - - describe('resourceTypeCompleter', () => { - it('should be defined as a completable schema', () => { - // The completer is a zod schema wrapped with completable - expect(resourceTypeCompleter).toBeDefined(); - // It should have the zod parse method - expect(typeof (resourceTypeCompleter as any).parse).toBe('function'); + describe('resourceTypeCompleter', () => { + it('should be defined as a completable schema', () => { + // The completer is a zod schema wrapped with completable + expect(resourceTypeCompleter).toBeDefined(); + // It should have the zod parse method + expect(typeof (resourceTypeCompleter as any).parse).toBe('function'); + }); + + it('should validate string resource types', () => { + // Test that valid strings pass validation + expect(() => (resourceTypeCompleter as any).parse('Text')).not.toThrow(); + expect(() => (resourceTypeCompleter as any).parse('Blob')).not.toThrow(); + }); }); - it('should validate string resource types', () => { - // Test that valid strings pass validation - expect(() => (resourceTypeCompleter as any).parse('Text')).not.toThrow(); - expect(() => (resourceTypeCompleter as any).parse('Blob')).not.toThrow(); + describe('resourceIdForPromptCompleter', () => { + it('should be defined as a completable schema', () => { + expect(resourceIdForPromptCompleter).toBeDefined(); + expect(typeof (resourceIdForPromptCompleter as any).parse).toBe('function'); + }); + + it('should validate string IDs', () => { + // Test that valid strings pass validation + expect(() => (resourceIdForPromptCompleter as any).parse('1')).not.toThrow(); + expect(() => (resourceIdForPromptCompleter as any).parse('100')).not.toThrow(); + }); }); - }); - describe('resourceIdForPromptCompleter', () => { - it('should be defined as a completable schema', () => { - expect(resourceIdForPromptCompleter).toBeDefined(); - expect(typeof (resourceIdForPromptCompleter as any).parse).toBe('function'); + describe('resourceIdForResourceTemplateCompleter', () => { + it('should validate positive integer IDs', () => { + expect(resourceIdForResourceTemplateCompleter('1')).toEqual(['1']); + expect(resourceIdForResourceTemplateCompleter('50')).toEqual(['50']); + }); + + it('should reject invalid IDs', () => { + expect(resourceIdForResourceTemplateCompleter('0')).toEqual([]); + expect(resourceIdForResourceTemplateCompleter('-5')).toEqual([]); + expect(resourceIdForResourceTemplateCompleter('not-a-number')).toEqual([]); + }); }); - it('should validate string IDs', () => { - // Test that valid strings pass validation - expect(() => (resourceIdForPromptCompleter as any).parse('1')).not.toThrow(); - expect(() => (resourceIdForPromptCompleter as any).parse('100')).not.toThrow(); - }); - }); + describe('registerResourceTemplates', () => { + it('should register text and blob resource templates', () => { + const registeredResources: any[] = []; - describe('resourceIdForResourceTemplateCompleter', () => { - it('should validate positive integer IDs', () => { - expect(resourceIdForResourceTemplateCompleter('1')).toEqual(['1']); - expect(resourceIdForResourceTemplateCompleter('50')).toEqual(['50']); - }); + const mockServer = { + registerResource: vi.fn((...args) => { + registeredResources.push(args); + }) + } as unknown as McpServer; - it('should reject invalid IDs', () => { - expect(resourceIdForResourceTemplateCompleter('0')).toEqual([]); - expect(resourceIdForResourceTemplateCompleter('-5')).toEqual([]); - expect(resourceIdForResourceTemplateCompleter('not-a-number')).toEqual([]); - }); - }); - - describe('registerResourceTemplates', () => { - it('should register text and blob resource templates', () => { - const registeredResources: any[] = []; - - const mockServer = { - registerResource: vi.fn((...args) => { - registeredResources.push(args); - }), - } as unknown as McpServer; - - registerResourceTemplates(mockServer); - - expect(mockServer.registerResource).toHaveBeenCalledTimes(2); - - // Check text resource registration - const textRegistration = registeredResources.find((r) => - r[0].includes('Text') - ); - expect(textRegistration).toBeDefined(); - expect(textRegistration[1]).toBeInstanceOf(ResourceTemplate); - - // Check blob resource registration - const blobRegistration = registeredResources.find((r) => - r[0].includes('Blob') - ); - expect(blobRegistration).toBeDefined(); - }); - }); -}); + registerResourceTemplates(mockServer); -describe('Session Resources', () => { - describe('getSessionResourceURI', () => { - it('should generate correct URI for resource name', () => { - expect(getSessionResourceURI('test')).toBe('demo://resource/session/test'); - }); + expect(mockServer.registerResource).toHaveBeenCalledTimes(2); - it('should handle various resource names', () => { - expect(getSessionResourceURI('my-file')).toBe('demo://resource/session/my-file'); - expect(getSessionResourceURI('document_123')).toBe( - 'demo://resource/session/document_123' - ); - }); - }); - - describe('registerSessionResource', () => { - it('should register text resource and return resource link', () => { - const registrations: any[] = []; - const mockServer = { - registerResource: vi.fn((...args) => { - registrations.push(args); - }), - } as unknown as McpServer; - - const resource = { - uri: 'demo://resource/session/test-file', - name: 'test-file', - mimeType: 'text/plain', - description: 'A test file', - }; - - const result = registerSessionResource( - mockServer, - resource, - 'text', - 'Hello, World!' - ); - - expect(result.type).toBe('resource_link'); - expect(result.uri).toBe(resource.uri); - expect(result.name).toBe(resource.name); - - expect(mockServer.registerResource).toHaveBeenCalledWith( - 'test-file', - 'demo://resource/session/test-file', - expect.objectContaining({ - mimeType: 'text/plain', - description: 'A test file', - }), - expect.any(Function) - ); - }); + // Check text resource registration + const textRegistration = registeredResources.find(r => r[0].includes('Text')); + expect(textRegistration).toBeDefined(); + expect(textRegistration[1]).toBeInstanceOf(ResourceTemplate); - it('should register blob resource correctly', () => { - const mockServer = { - registerResource: vi.fn(), - } as unknown as McpServer; - - const resource = { - uri: 'demo://resource/session/binary-file', - name: 'binary-file', - mimeType: 'application/octet-stream', - }; - - const blobContent = Buffer.from('binary data').toString('base64'); - const result = registerSessionResource(mockServer, resource, 'blob', blobContent); - - expect(result.type).toBe('resource_link'); - expect(mockServer.registerResource).toHaveBeenCalled(); + // Check blob resource registration + const blobRegistration = registeredResources.find(r => r[0].includes('Blob')); + expect(blobRegistration).toBeDefined(); + }); }); +}); - it('should return resource handler that provides correct content', async () => { - let capturedHandler: Function | null = null; - const mockServer = { - registerResource: vi.fn((_name, _uri, _config, handler) => { - capturedHandler = handler; - }), - } as unknown as McpServer; - - const resource = { - uri: 'demo://resource/session/content-test', - name: 'content-test', - mimeType: 'text/plain', - }; - - registerSessionResource(mockServer, resource, 'text', 'Test content here'); - - expect(capturedHandler).not.toBeNull(); +describe('Session Resources', () => { + describe('getSessionResourceURI', () => { + it('should generate correct URI for resource name', () => { + expect(getSessionResourceURI('test')).toBe('demo://resource/session/test'); + }); + + it('should handle various resource names', () => { + expect(getSessionResourceURI('my-file')).toBe('demo://resource/session/my-file'); + expect(getSessionResourceURI('document_123')).toBe('demo://resource/session/document_123'); + }); + }); - const handlerResult = await capturedHandler!(new URL(resource.uri)); - expect(handlerResult.contents).toHaveLength(1); - expect(handlerResult.contents[0].text).toBe('Test content here'); - expect(handlerResult.contents[0].mimeType).toBe('text/plain'); + describe('registerSessionResource', () => { + it('should register text resource and return resource link', () => { + const registrations: any[] = []; + const mockServer = { + registerResource: vi.fn((...args) => { + registrations.push(args); + }) + } as unknown as McpServer; + + const resource = { + uri: 'demo://resource/session/test-file', + name: 'test-file', + mimeType: 'text/plain', + description: 'A test file' + }; + + const result = registerSessionResource(mockServer, resource, 'text', 'Hello, World!'); + + expect(result.type).toBe('resource_link'); + expect(result.uri).toBe(resource.uri); + expect(result.name).toBe(resource.name); + + expect(mockServer.registerResource).toHaveBeenCalledWith( + 'test-file', + 'demo://resource/session/test-file', + expect.objectContaining({ + mimeType: 'text/plain', + description: 'A test file' + }), + expect.any(Function) + ); + }); + + it('should register blob resource correctly', () => { + const mockServer = { + registerResource: vi.fn() + } as unknown as McpServer; + + const resource = { + uri: 'demo://resource/session/binary-file', + name: 'binary-file', + mimeType: 'application/octet-stream' + }; + + const blobContent = Buffer.from('binary data').toString('base64'); + const result = registerSessionResource(mockServer, resource, 'blob', blobContent); + + expect(result.type).toBe('resource_link'); + expect(mockServer.registerResource).toHaveBeenCalled(); + }); + + it('should return resource handler that provides correct content', async () => { + let capturedHandler: Function | null = null; + const mockServer = { + registerResource: vi.fn((_name, _uri, _config, handler) => { + capturedHandler = handler; + }) + } as unknown as McpServer; + + const resource = { + uri: 'demo://resource/session/content-test', + name: 'content-test', + mimeType: 'text/plain' + }; + + registerSessionResource(mockServer, resource, 'text', 'Test content here'); + + expect(capturedHandler).not.toBeNull(); + + const handlerResult = await capturedHandler!(new URL(resource.uri)); + expect(handlerResult.contents).toHaveLength(1); + expect(handlerResult.contents[0].text).toBe('Test content here'); + expect(handlerResult.contents[0].mimeType).toBe('text/plain'); + }); }); - }); }); describe('File Resources', () => { - describe('registerFileResources', () => { - it('should register file resources when docs directory exists', () => { - const mockServer = { - registerResource: vi.fn(), - } as unknown as McpServer; - - registerFileResources(mockServer); - - // The docs folder exists in the everything server and contains files - // so registerResource should have been called - expect(mockServer.registerResource).toHaveBeenCalled(); + describe('registerFileResources', () => { + it('should register file resources when docs directory exists', () => { + const mockServer = { + registerResource: vi.fn() + } as unknown as McpServer; + + registerFileResources(mockServer); + + // The docs folder exists in the everything server and contains files + // so registerResource should have been called + expect(mockServer.registerResource).toHaveBeenCalled(); + }); }); - }); }); describe('Subscriptions', () => { - describe('setSubscriptionHandlers', () => { - it('should set request handlers on server', () => { - const mockServer = { - server: { - setRequestHandler: vi.fn(), - }, - sendLoggingMessage: vi.fn(), - } as unknown as McpServer; - - setSubscriptionHandlers(mockServer); - - // Should set both subscribe and unsubscribe handlers - expect(mockServer.server.setRequestHandler).toHaveBeenCalledTimes(2); - }); - }); - - describe('simulated resource updates lifecycle', () => { - afterEach(() => { - // Clean up any intervals - stopSimulatedResourceUpdates('lifecycle-test-session'); + describe('setSubscriptionHandlers', () => { + it('should set request handlers on server', () => { + const mockServer = { + server: { + setRequestHandler: vi.fn() + }, + sendLoggingMessage: vi.fn() + } as unknown as McpServer; + + setSubscriptionHandlers(mockServer); + + // Should set both subscribe and unsubscribe handlers + expect(mockServer.server.setRequestHandler).toHaveBeenCalledTimes(2); + }); }); - it('should start and stop updates without errors', () => { - const mockServer = { - server: { - notification: vi.fn(), - }, - } as unknown as McpServer; - - // Start updates - should work for both defined and undefined sessionId - beginSimulatedResourceUpdates(mockServer, 'lifecycle-test-session'); - beginSimulatedResourceUpdates(mockServer, undefined); - - // Stop updates - should handle all cases gracefully - stopSimulatedResourceUpdates('lifecycle-test-session'); - stopSimulatedResourceUpdates('non-existent-session'); - stopSimulatedResourceUpdates(undefined); - - // If we got here without throwing, the lifecycle works correctly - expect(true).toBe(true); + describe('simulated resource updates lifecycle', () => { + afterEach(() => { + // Clean up any intervals + stopSimulatedResourceUpdates('lifecycle-test-session'); + }); + + it('should start and stop updates without errors', () => { + const mockServer = { + server: { + notification: vi.fn() + } + } as unknown as McpServer; + + // Start updates - should work for both defined and undefined sessionId + beginSimulatedResourceUpdates(mockServer, 'lifecycle-test-session'); + beginSimulatedResourceUpdates(mockServer, undefined); + + // Stop updates - should handle all cases gracefully + stopSimulatedResourceUpdates('lifecycle-test-session'); + stopSimulatedResourceUpdates('non-existent-session'); + stopSimulatedResourceUpdates(undefined); + + // If we got here without throwing, the lifecycle works correctly + expect(true).toBe(true); + }); }); - }); }); diff --git a/src/everything/__tests__/server.test.ts b/src/everything/__tests__/server.test.ts index e7985dd982..15ad7640ef 100644 --- a/src/everything/__tests__/server.test.ts +++ b/src/everything/__tests__/server.test.ts @@ -2,40 +2,40 @@ import { describe, it, expect, vi } from 'vitest'; import { createServer } from '../server/index.js'; describe('Server Factory', () => { - describe('createServer', () => { - it('should return a ServerFactoryResponse object', () => { - const result = createServer(); + describe('createServer', () => { + it('should return a ServerFactoryResponse object', () => { + const result = createServer(); - expect(result).toHaveProperty('server'); - expect(result).toHaveProperty('cleanup'); - }); + expect(result).toHaveProperty('server'); + expect(result).toHaveProperty('cleanup'); + }); - it('should return a cleanup function', () => { - const { cleanup } = createServer(); + it('should return a cleanup function', () => { + const { cleanup } = createServer(); - expect(typeof cleanup).toBe('function'); - }); + expect(typeof cleanup).toBe('function'); + }); - it('should create an McpServer instance', () => { - const { server } = createServer(); + it('should create an McpServer instance', () => { + const { server } = createServer(); - expect(server).toBeDefined(); - expect(server.server).toBeDefined(); - }); + expect(server).toBeDefined(); + expect(server.server).toBeDefined(); + }); - it('should have an oninitialized handler set', () => { - const { server } = createServer(); + it('should have an oninitialized handler set', () => { + const { server } = createServer(); - expect(server.server.oninitialized).toBeDefined(); - }); + expect(server.server.oninitialized).toBeDefined(); + }); - it('should allow multiple servers to be created', () => { - const result1 = createServer(); - const result2 = createServer(); + it('should allow multiple servers to be created', () => { + const result1 = createServer(); + const result2 = createServer(); - expect(result1.server).toBeDefined(); - expect(result2.server).toBeDefined(); - expect(result1.server).not.toBe(result2.server); + expect(result1.server).toBeDefined(); + expect(result2.server).toBeDefined(); + expect(result1.server).not.toBe(result2.server); + }); }); - }); }); diff --git a/src/everything/__tests__/tools.test.ts b/src/everything/__tests__/tools.test.ts index a50bbd6592..4ab313962a 100644 --- a/src/everything/__tests__/tools.test.ts +++ b/src/everything/__tests__/tools.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { McpServer } from '@modelcontextprotocol/server'; import { registerEchoTool, EchoSchema } from '../tools/echo.js'; import { registerGetSumTool } from '../tools/get-sum.js'; import { registerGetEnvTool } from '../tools/get-env.js'; @@ -13,1209 +13,1089 @@ import { registerToggleSimulatedLoggingTool } from '../tools/toggle-simulated-lo import { registerToggleSubscriberUpdatesTool } from '../tools/toggle-subscriber-updates.js'; import { registerTriggerSamplingRequestTool } from '../tools/trigger-sampling-request.js'; import { registerTriggerElicitationRequestTool } from '../tools/trigger-elicitation-request.js'; -import { - registerTriggerUrlElicitationTool, - __resetIssuedErrorPathElicitations, -} from '../tools/trigger-url-elicitation.js'; +import { registerTriggerUrlElicitationTool, __resetIssuedErrorPathElicitations } from '../tools/trigger-url-elicitation.js'; import { registerGetRootsListTool } from '../tools/get-roots-list.js'; import { registerGZipFileAsResourceTool } from '../tools/gzip-file-as-resource.js'; -import { registerSimulateResearchQueryTool } from '../tools/simulate-research-query.js'; // Helper to capture registered tool handlers function createMockServer() { - const handlers: Map = new Map(); - const configs: Map = new Map(); - - const mockServer = { - registerTool: vi.fn((name: string, config: any, handler: Function) => { - handlers.set(name, handler); - configs.set(name, config); - }), - server: { - getClientCapabilities: vi.fn(() => ({})), - notification: vi.fn(), - }, - sendLoggingMessage: vi.fn(), - sendResourceUpdated: vi.fn(), - } as unknown as McpServer; - - return { mockServer, handlers, configs }; -} - -describe('Tools', () => { - describe('echo', () => { - it('should echo back the message', async () => { - const { mockServer, handlers } = createMockServer(); - registerEchoTool(mockServer); - - const handler = handlers.get('echo')!; - const result = await handler({ message: 'Hello, World!' }); - - expect(result).toEqual({ - content: [{ type: 'text', text: 'Echo: Hello, World!' }], - }); - }); - - it('should handle empty message', async () => { - const { mockServer, handlers } = createMockServer(); - registerEchoTool(mockServer); - - const handler = handlers.get('echo')!; - const result = await handler({ message: '' }); - - expect(result).toEqual({ - content: [{ type: 'text', text: 'Echo: ' }], - }); - }); - - it('should reject invalid input', async () => { - const { mockServer, handlers } = createMockServer(); - registerEchoTool(mockServer); - - const handler = handlers.get('echo')!; - - await expect(handler({})).rejects.toThrow(); - await expect(handler({ message: 123 })).rejects.toThrow(); - }); - }); - - describe('EchoSchema', () => { - it('should validate correct input', () => { - const result = EchoSchema.parse({ message: 'test' }); - expect(result).toEqual({ message: 'test' }); - }); - - it('should reject missing message', () => { - expect(() => EchoSchema.parse({})).toThrow(); - }); - - it('should reject non-string message', () => { - expect(() => EchoSchema.parse({ message: 123 })).toThrow(); - }); - }); - - describe('get-sum', () => { - it('should calculate sum of two positive numbers', async () => { - const { mockServer, handlers } = createMockServer(); - registerGetSumTool(mockServer); - - const handler = handlers.get('get-sum')!; - const result = await handler({ a: 5, b: 3 }); - - expect(result).toEqual({ - content: [{ type: 'text', text: 'The sum of 5 and 3 is 8.' }], - }); - }); - - it('should calculate sum with negative numbers', async () => { - const { mockServer, handlers } = createMockServer(); - registerGetSumTool(mockServer); - - const handler = handlers.get('get-sum')!; - const result = await handler({ a: -5, b: 3 }); - - expect(result).toEqual({ - content: [{ type: 'text', text: 'The sum of -5 and 3 is -2.' }], - }); - }); - - it('should calculate sum with zero', async () => { - const { mockServer, handlers } = createMockServer(); - registerGetSumTool(mockServer); - - const handler = handlers.get('get-sum')!; - const result = await handler({ a: 0, b: 0 }); - - expect(result).toEqual({ - content: [{ type: 'text', text: 'The sum of 0 and 0 is 0.' }], - }); - }); - - it('should handle floating point numbers', async () => { - const { mockServer, handlers } = createMockServer(); - registerGetSumTool(mockServer); - - const handler = handlers.get('get-sum')!; - const result = await handler({ a: 1.5, b: 2.5 }); - - expect(result).toEqual({ - content: [{ type: 'text', text: 'The sum of 1.5 and 2.5 is 4.' }], - }); - }); - - it('should reject invalid input', async () => { - const { mockServer, handlers } = createMockServer(); - registerGetSumTool(mockServer); - - const handler = handlers.get('get-sum')!; - - await expect(handler({})).rejects.toThrow(); - await expect(handler({ a: 'not a number', b: 5 })).rejects.toThrow(); - await expect(handler({ a: 5 })).rejects.toThrow(); - }); - }); - - describe('get-env', () => { - it('should return all environment variables as JSON', async () => { - const { mockServer, handlers } = createMockServer(); - registerGetEnvTool(mockServer); - - const handler = handlers.get('get-env')!; - process.env.TEST_VAR_EVERYTHING = 'test_value'; - const result = await handler({}); - - expect(result.content).toHaveLength(1); - expect(result.content[0].type).toBe('text'); - - const envJson = JSON.parse(result.content[0].text); - expect(envJson.TEST_VAR_EVERYTHING).toBe('test_value'); - - delete process.env.TEST_VAR_EVERYTHING; - }); - - it('should return valid JSON', async () => { - const { mockServer, handlers } = createMockServer(); - registerGetEnvTool(mockServer); - - const handler = handlers.get('get-env')!; - const result = await handler({}); - - expect(() => JSON.parse(result.content[0].text)).not.toThrow(); - }); - }); - - describe('get-tiny-image', () => { - it('should return image content with text descriptions', async () => { - const { mockServer, handlers } = createMockServer(); - registerGetTinyImageTool(mockServer); - - const handler = handlers.get('get-tiny-image')!; - const result = await handler({}); - - expect(result.content).toHaveLength(3); - expect(result.content[0]).toEqual({ - type: 'text', - text: "Here's the image you requested:", - }); - expect(result.content[1]).toEqual({ - type: 'image', - data: MCP_TINY_IMAGE, - mimeType: 'image/png', - }); - expect(result.content[2]).toEqual({ - type: 'text', - text: 'The image above is the MCP logo.', - }); - }); - - it('should return valid base64 image data', async () => { - const { mockServer, handlers } = createMockServer(); - registerGetTinyImageTool(mockServer); - - const handler = handlers.get('get-tiny-image')!; - const result = await handler({}); - - const imageContent = result.content[1]; - expect(imageContent.type).toBe('image'); - expect(imageContent.mimeType).toBe('image/png'); - // Verify it's valid base64 - expect(() => Buffer.from(imageContent.data, 'base64')).not.toThrow(); - }); - }); - - describe('get-structured-content', () => { - it('should return weather for New York', async () => { - const { mockServer, handlers } = createMockServer(); - registerGetStructuredContentTool(mockServer); - - const handler = handlers.get('get-structured-content')!; - const result = await handler({ location: 'New York' }); - - expect(result.structuredContent).toEqual({ - temperature: 33, - conditions: 'Cloudy', - humidity: 82, - }); - expect(result.content[0].type).toBe('text'); - expect(JSON.parse(result.content[0].text)).toEqual(result.structuredContent); - }); - - it('should return weather for Chicago', async () => { - const { mockServer, handlers } = createMockServer(); - registerGetStructuredContentTool(mockServer); - - const handler = handlers.get('get-structured-content')!; - const result = await handler({ location: 'Chicago' }); + const handlers: Map = new Map(); + const configs: Map = new Map(); - expect(result.structuredContent).toEqual({ - temperature: 36, - conditions: 'Light rain / drizzle', - humidity: 82, - }); - }); - - it('should return weather for Los Angeles', async () => { - const { mockServer, handlers } = createMockServer(); - registerGetStructuredContentTool(mockServer); - - const handler = handlers.get('get-structured-content')!; - const result = await handler({ location: 'Los Angeles' }); - - expect(result.structuredContent).toEqual({ - temperature: 73, - conditions: 'Sunny / Clear', - humidity: 48, - }); - }); - }); - - describe('get-annotated-message', () => { - it('should return error message with high priority', async () => { - const { mockServer, handlers } = createMockServer(); - registerGetAnnotatedMessageTool(mockServer); - - const handler = handlers.get('get-annotated-message')!; - const result = await handler({ messageType: 'error', includeImage: false }); - - expect(result.content).toHaveLength(1); - expect(result.content[0].text).toBe('Error: Operation failed'); - expect(result.content[0].annotations).toEqual({ - priority: 1.0, - audience: ['user', 'assistant'], - }); - }); - - it('should return success message with medium priority', async () => { - const { mockServer, handlers } = createMockServer(); - registerGetAnnotatedMessageTool(mockServer); - - const handler = handlers.get('get-annotated-message')!; - const result = await handler({ messageType: 'success', includeImage: false }); - - expect(result.content[0].text).toBe('Operation completed successfully'); - expect(result.content[0].annotations.priority).toBe(0.7); - expect(result.content[0].annotations.audience).toEqual(['user']); - }); - - it('should return debug message with low priority', async () => { - const { mockServer, handlers } = createMockServer(); - registerGetAnnotatedMessageTool(mockServer); - - const handler = handlers.get('get-annotated-message')!; - const result = await handler({ messageType: 'debug', includeImage: false }); - - expect(result.content[0].text).toContain('Debug:'); - expect(result.content[0].annotations.priority).toBe(0.3); - expect(result.content[0].annotations.audience).toEqual(['assistant']); - }); - - it('should include annotated image when requested', async () => { - const { mockServer, handlers } = createMockServer(); - registerGetAnnotatedMessageTool(mockServer); - - const handler = handlers.get('get-annotated-message')!; - const result = await handler({ messageType: 'success', includeImage: true }); - - expect(result.content).toHaveLength(2); - expect(result.content[1].type).toBe('image'); - expect(result.content[1].annotations).toEqual({ - priority: 0.5, - audience: ['user'], - }); - }); - }); - - describe('trigger-long-running-operation', () => { - it('should complete operation and return result', async () => { - const { mockServer, handlers } = createMockServer(); - registerTriggerLongRunningOperationTool(mockServer); - - const handler = handlers.get('trigger-long-running-operation')!; - // Use very short duration for test - const result = await handler( - { duration: 0.1, steps: 2 }, - { _meta: {}, requestId: 'test-123' } - ); - - expect(result.content[0].text).toContain('Long running operation completed'); - expect(result.content[0].text).toContain('Duration: 0.1 seconds'); - expect(result.content[0].text).toContain('Steps: 2'); - }, 10000); - - it('should send progress notifications when progressToken provided', async () => { - const { mockServer, handlers } = createMockServer(); - registerTriggerLongRunningOperationTool(mockServer); - - const handler = handlers.get('trigger-long-running-operation')!; - await handler( - { duration: 0.1, steps: 2 }, - { _meta: { progressToken: 'token-123' }, requestId: 'test-456', sessionId: 'session-1' } - ); - - expect(mockServer.server.notification).toHaveBeenCalledTimes(2); - expect(mockServer.server.notification).toHaveBeenCalledWith( - expect.objectContaining({ - method: 'notifications/progress', - params: expect.objectContaining({ - progressToken: 'token-123', - }), + const mockServer = { + registerTool: vi.fn((name: string, config: any, handler: Function) => { + handlers.set(name, handler); + configs.set(name, config); }), - expect.any(Object) - ); - }, 10000); - }); - - describe('get-resource-links', () => { - it('should return specified number of resource links', async () => { - const { mockServer, handlers } = createMockServer(); - registerGetResourceLinksTool(mockServer); - - const handler = handlers.get('get-resource-links')!; - const result = await handler({ count: 3 }); - - // 1 intro text + 3 resource links - expect(result.content).toHaveLength(4); - expect(result.content[0].type).toBe('text'); - expect(result.content[0].text).toContain('3 resource links'); - - // Check resource links - for (let i = 1; i < 4; i++) { - expect(result.content[i].type).toBe('resource_link'); - expect(result.content[i].uri).toBeDefined(); - expect(result.content[i].name).toBeDefined(); - } - }); - - it('should alternate between text and blob resources', async () => { - const { mockServer, handlers } = createMockServer(); - registerGetResourceLinksTool(mockServer); - - const handler = handlers.get('get-resource-links')!; - const result = await handler({ count: 4 }); - - // Odd IDs (1, 3) are blob, even IDs (2, 4) are text - expect(result.content[1].name).toContain('Blob'); - expect(result.content[2].name).toContain('Text'); - expect(result.content[3].name).toContain('Blob'); - expect(result.content[4].name).toContain('Text'); - }); - - it('should use default count of 3', async () => { - const { mockServer, handlers } = createMockServer(); - registerGetResourceLinksTool(mockServer); - - const handler = handlers.get('get-resource-links')!; - const result = await handler({}); + server: { + getClientCapabilities: vi.fn(() => ({})), + notification: vi.fn() + }, + sendLoggingMessage: vi.fn(), + sendResourceUpdated: vi.fn() + } as unknown as McpServer; - // 1 intro text + 3 resource links (default) - expect(result.content).toHaveLength(4); - }); - }); + return { mockServer, handlers, configs }; +} - describe('get-resource-reference', () => { - it('should return text resource reference', async () => { - const { mockServer, handlers } = createMockServer(); - registerGetResourceReferenceTool(mockServer); +describe('Tools', () => { + describe('echo', () => { + it('should echo back the message', async () => { + const { mockServer, handlers } = createMockServer(); + registerEchoTool(mockServer); - const handler = handlers.get('get-resource-reference')!; - const result = await handler({ resourceType: 'Text', resourceId: 1 }); + const handler = handlers.get('echo')!; + const result = await handler({ message: 'Hello, World!' }); - expect(result.content).toHaveLength(3); - expect(result.content[0].text).toContain('Resource 1'); - expect(result.content[1].type).toBe('resource'); - expect(result.content[1].resource.uri).toContain('text/1'); - expect(result.content[2].text).toContain('URI'); - }); + expect(result).toEqual({ + content: [{ type: 'text', text: 'Echo: Hello, World!' }] + }); + }); - it('should return blob resource reference', async () => { - const { mockServer, handlers } = createMockServer(); - registerGetResourceReferenceTool(mockServer); + it('should handle empty message', async () => { + const { mockServer, handlers } = createMockServer(); + registerEchoTool(mockServer); - const handler = handlers.get('get-resource-reference')!; - const result = await handler({ resourceType: 'Blob', resourceId: 5 }); + const handler = handlers.get('echo')!; + const result = await handler({ message: '' }); - expect(result.content[1].resource.uri).toContain('blob/5'); - }); + expect(result).toEqual({ + content: [{ type: 'text', text: 'Echo: ' }] + }); + }); - it('should reject invalid resource type', async () => { - const { mockServer, handlers } = createMockServer(); - registerGetResourceReferenceTool(mockServer); + it('should reject invalid input', async () => { + const { mockServer, handlers } = createMockServer(); + registerEchoTool(mockServer); - const handler = handlers.get('get-resource-reference')!; - await expect(handler({ resourceType: 'Invalid', resourceId: 1 })).rejects.toThrow( - 'Invalid resourceType' - ); - }); + const handler = handlers.get('echo')!; - it('should reject invalid resource ID', async () => { - const { mockServer, handlers } = createMockServer(); - registerGetResourceReferenceTool(mockServer); - - const handler = handlers.get('get-resource-reference')!; - await expect(handler({ resourceType: 'Text', resourceId: -1 })).rejects.toThrow( - 'Invalid resourceId' - ); - await expect(handler({ resourceType: 'Text', resourceId: 0 })).rejects.toThrow( - 'Invalid resourceId' - ); - await expect(handler({ resourceType: 'Text', resourceId: 1.5 })).rejects.toThrow( - 'Invalid resourceId' - ); + await expect(handler({})).rejects.toThrow(); + await expect(handler({ message: 123 })).rejects.toThrow(); + }); }); - }); - describe('toggle-simulated-logging', () => { - it('should start logging when not active', async () => { - const { mockServer, handlers } = createMockServer(); - registerToggleSimulatedLoggingTool(mockServer); + describe('EchoSchema', () => { + it('should validate correct input', () => { + const result = EchoSchema.parse({ message: 'test' }); + expect(result).toEqual({ message: 'test' }); + }); - const handler = handlers.get('toggle-simulated-logging')!; - const result = await handler({}, { sessionId: 'test-session-1' }); + it('should reject missing message', () => { + expect(() => EchoSchema.parse({})).toThrow(); + }); - expect(result.content[0].text).toContain('Started'); - expect(result.content[0].text).toContain('test-session-1'); + it('should reject non-string message', () => { + expect(() => EchoSchema.parse({ message: 123 })).toThrow(); + }); }); - it('should stop logging when already active', async () => { - const { mockServer, handlers } = createMockServer(); - registerToggleSimulatedLoggingTool(mockServer); - - const handler = handlers.get('toggle-simulated-logging')!; + describe('get-sum', () => { + it('should calculate sum of two positive numbers', async () => { + const { mockServer, handlers } = createMockServer(); + registerGetSumTool(mockServer); - // First call starts logging - await handler({}, { sessionId: 'test-session-2' }); + const handler = handlers.get('get-sum')!; + const result = await handler({ a: 5, b: 3 }); - // Second call stops logging - const result = await handler({}, { sessionId: 'test-session-2' }); + expect(result).toEqual({ + content: [{ type: 'text', text: 'The sum of 5 and 3 is 8.' }] + }); + }); - expect(result.content[0].text).toContain('Stopped'); - expect(result.content[0].text).toContain('test-session-2'); - }); + it('should calculate sum with negative numbers', async () => { + const { mockServer, handlers } = createMockServer(); + registerGetSumTool(mockServer); - it('should handle undefined sessionId', async () => { - const { mockServer, handlers } = createMockServer(); - registerToggleSimulatedLoggingTool(mockServer); + const handler = handlers.get('get-sum')!; + const result = await handler({ a: -5, b: 3 }); - const handler = handlers.get('toggle-simulated-logging')!; - const result = await handler({}, {}); + expect(result).toEqual({ + content: [{ type: 'text', text: 'The sum of -5 and 3 is -2.' }] + }); + }); - expect(result.content[0].text).toContain('Started'); - }); - }); + it('should calculate sum with zero', async () => { + const { mockServer, handlers } = createMockServer(); + registerGetSumTool(mockServer); - describe('toggle-subscriber-updates', () => { - it('should start updates when not active', async () => { - const { mockServer, handlers } = createMockServer(); - registerToggleSubscriberUpdatesTool(mockServer); + const handler = handlers.get('get-sum')!; + const result = await handler({ a: 0, b: 0 }); - const handler = handlers.get('toggle-subscriber-updates')!; - const result = await handler({}, { sessionId: 'sub-session-1' }); + expect(result).toEqual({ + content: [{ type: 'text', text: 'The sum of 0 and 0 is 0.' }] + }); + }); - expect(result.content[0].text).toContain('Started'); - expect(result.content[0].text).toContain('sub-session-1'); - }); + it('should handle floating point numbers', async () => { + const { mockServer, handlers } = createMockServer(); + registerGetSumTool(mockServer); - it('should stop updates when already active', async () => { - const { mockServer, handlers } = createMockServer(); - registerToggleSubscriberUpdatesTool(mockServer); + const handler = handlers.get('get-sum')!; + const result = await handler({ a: 1.5, b: 2.5 }); - const handler = handlers.get('toggle-subscriber-updates')!; + expect(result).toEqual({ + content: [{ type: 'text', text: 'The sum of 1.5 and 2.5 is 4.' }] + }); + }); - // First call starts updates - await handler({}, { sessionId: 'sub-session-2' }); + it('should reject invalid input', async () => { + const { mockServer, handlers } = createMockServer(); + registerGetSumTool(mockServer); - // Second call stops updates - const result = await handler({}, { sessionId: 'sub-session-2' }); + const handler = handlers.get('get-sum')!; - expect(result.content[0].text).toContain('Stopped'); - expect(result.content[0].text).toContain('sub-session-2'); + await expect(handler({})).rejects.toThrow(); + await expect(handler({ a: 'not a number', b: 5 })).rejects.toThrow(); + await expect(handler({ a: 5 })).rejects.toThrow(); + }); }); - }); - describe('trigger-sampling-request', () => { - it('should not register when client does not support sampling', () => { - const { mockServer } = createMockServer(); - registerTriggerSamplingRequestTool(mockServer); - - // Tool should not be registered since mock server returns empty capabilities - expect(mockServer.registerTool).not.toHaveBeenCalled(); - }); + describe('get-env', () => { + it('should return all environment variables as JSON', async () => { + const { mockServer, handlers } = createMockServer(); + registerGetEnvTool(mockServer); - it('should register when client supports sampling', () => { - const handlers: Map = new Map(); - const mockServer = { - registerTool: vi.fn((name: string, config: any, handler: Function) => { - handlers.set(name, handler); - }), - server: { - getClientCapabilities: vi.fn(() => ({ sampling: {} })), - }, - } as unknown as McpServer; + const handler = handlers.get('get-env')!; + process.env.TEST_VAR_EVERYTHING = 'test_value'; + const result = await handler({}); - registerTriggerSamplingRequestTool(mockServer); + expect(result.content).toHaveLength(1); + expect(result.content[0].type).toBe('text'); - expect(mockServer.registerTool).toHaveBeenCalledWith( - 'trigger-sampling-request', - expect.objectContaining({ - title: 'Trigger Sampling Request Tool', - description: expect.stringContaining('Sampling'), - }), - expect.any(Function) - ); - }); + const envJson = JSON.parse(result.content[0].text); + expect(envJson.TEST_VAR_EVERYTHING).toBe('test_value'); - it('should send sampling request and return result', async () => { - const handlers: Map = new Map(); - const mockSendRequest = vi.fn().mockResolvedValue({ - model: 'test-model', - content: { type: 'text', text: 'LLM response' }, - }); + delete process.env.TEST_VAR_EVERYTHING; + }); - const mockServer = { - registerTool: vi.fn((name: string, config: any, handler: Function) => { - handlers.set(name, handler); - }), - server: { - getClientCapabilities: vi.fn(() => ({ sampling: {} })), - }, - } as unknown as McpServer; - - registerTriggerSamplingRequestTool(mockServer); - - const handler = handlers.get('trigger-sampling-request')!; - const result = await handler( - { prompt: 'Test prompt', maxTokens: 50 }, - { sendRequest: mockSendRequest } - ); - - expect(mockSendRequest).toHaveBeenCalledWith( - expect.objectContaining({ - method: 'sampling/createMessage', - params: expect.objectContaining({ - maxTokens: 50, - }), - }), - expect.anything() - ); - expect(result.content[0].text).toContain('LLM sampling result'); - }); - }); + it('should return valid JSON', async () => { + const { mockServer, handlers } = createMockServer(); + registerGetEnvTool(mockServer); - describe('trigger-elicitation-request', () => { - it('should not register when client does not support elicitation', () => { - const { mockServer } = createMockServer(); - registerTriggerElicitationRequestTool(mockServer); + const handler = handlers.get('get-env')!; + const result = await handler({}); - expect(mockServer.registerTool).not.toHaveBeenCalled(); + expect(() => JSON.parse(result.content[0].text)).not.toThrow(); + }); }); - it('should register when client supports elicitation', () => { - const handlers: Map = new Map(); - const mockServer = { - registerTool: vi.fn((name: string, config: any, handler: Function) => { - handlers.set(name, handler); - }), - server: { - getClientCapabilities: vi.fn(() => ({ elicitation: {} })), - }, - } as unknown as McpServer; - - registerTriggerElicitationRequestTool(mockServer); - - expect(mockServer.registerTool).toHaveBeenCalledWith( - 'trigger-elicitation-request', - expect.objectContaining({ - title: 'Trigger Elicitation Request Tool', - description: expect.stringContaining('Elicitation'), - }), - expect.any(Function) - ); + describe('get-tiny-image', () => { + it('should return image content with text descriptions', async () => { + const { mockServer, handlers } = createMockServer(); + registerGetTinyImageTool(mockServer); + + const handler = handlers.get('get-tiny-image')!; + const result = await handler({}); + + expect(result.content).toHaveLength(3); + expect(result.content[0]).toEqual({ + type: 'text', + text: "Here's the image you requested:" + }); + expect(result.content[1]).toEqual({ + type: 'image', + data: MCP_TINY_IMAGE, + mimeType: 'image/png' + }); + expect(result.content[2]).toEqual({ + type: 'text', + text: 'The image above is the MCP logo.' + }); + }); + + it('should return valid base64 image data', async () => { + const { mockServer, handlers } = createMockServer(); + registerGetTinyImageTool(mockServer); + + const handler = handlers.get('get-tiny-image')!; + const result = await handler({}); + + const imageContent = result.content[1]; + expect(imageContent.type).toBe('image'); + expect(imageContent.mimeType).toBe('image/png'); + // Verify it's valid base64 + expect(() => Buffer.from(imageContent.data, 'base64')).not.toThrow(); + }); }); - it('should handle accept action with user content', async () => { - const handlers: Map = new Map(); - const mockSendRequest = vi.fn().mockResolvedValue({ - action: 'accept', - content: { - name: 'John Doe', - check: true, - email: 'john@example.com', - }, - }); - - const mockServer = { - registerTool: vi.fn((name: string, config: any, handler: Function) => { - handlers.set(name, handler); - }), - server: { - getClientCapabilities: vi.fn(() => ({ elicitation: {} })), - }, - } as unknown as McpServer; - - registerTriggerElicitationRequestTool(mockServer); - - const handler = handlers.get('trigger-elicitation-request')!; - const result = await handler({}, { sendRequest: mockSendRequest }); - - expect(result.content[0].text).toContain('✅'); - expect(result.content[0].text).toContain('provided'); - expect(result.content[1].text).toContain('John Doe'); + describe('get-structured-content', () => { + it('should return weather for New York', async () => { + const { mockServer, handlers } = createMockServer(); + registerGetStructuredContentTool(mockServer); + + const handler = handlers.get('get-structured-content')!; + const result = await handler({ location: 'New York' }); + + expect(result.structuredContent).toEqual({ + temperature: 33, + conditions: 'Cloudy', + humidity: 82 + }); + expect(result.content[0].type).toBe('text'); + expect(JSON.parse(result.content[0].text)).toEqual(result.structuredContent); + }); + + it('should return weather for Chicago', async () => { + const { mockServer, handlers } = createMockServer(); + registerGetStructuredContentTool(mockServer); + + const handler = handlers.get('get-structured-content')!; + const result = await handler({ location: 'Chicago' }); + + expect(result.structuredContent).toEqual({ + temperature: 36, + conditions: 'Light rain / drizzle', + humidity: 82 + }); + }); + + it('should return weather for Los Angeles', async () => { + const { mockServer, handlers } = createMockServer(); + registerGetStructuredContentTool(mockServer); + + const handler = handlers.get('get-structured-content')!; + const result = await handler({ location: 'Los Angeles' }); + + expect(result.structuredContent).toEqual({ + temperature: 73, + conditions: 'Sunny / Clear', + humidity: 48 + }); + }); }); - it('should handle decline action', async () => { - const handlers: Map = new Map(); - const mockSendRequest = vi.fn().mockResolvedValue({ - action: 'decline', - }); - - const mockServer = { - registerTool: vi.fn((name: string, config: any, handler: Function) => { - handlers.set(name, handler); - }), - server: { - getClientCapabilities: vi.fn(() => ({ elicitation: {} })), - }, - } as unknown as McpServer; - - registerTriggerElicitationRequestTool(mockServer); - - const handler = handlers.get('trigger-elicitation-request')!; - const result = await handler({}, { sendRequest: mockSendRequest }); - - expect(result.content[0].text).toContain('❌'); - expect(result.content[0].text).toContain('declined'); + describe('get-annotated-message', () => { + it('should return error message with high priority', async () => { + const { mockServer, handlers } = createMockServer(); + registerGetAnnotatedMessageTool(mockServer); + + const handler = handlers.get('get-annotated-message')!; + const result = await handler({ messageType: 'error', includeImage: false }); + + expect(result.content).toHaveLength(1); + expect(result.content[0].text).toBe('Error: Operation failed'); + expect(result.content[0].annotations).toEqual({ + priority: 1.0, + audience: ['user', 'assistant'] + }); + }); + + it('should return success message with medium priority', async () => { + const { mockServer, handlers } = createMockServer(); + registerGetAnnotatedMessageTool(mockServer); + + const handler = handlers.get('get-annotated-message')!; + const result = await handler({ messageType: 'success', includeImage: false }); + + expect(result.content[0].text).toBe('Operation completed successfully'); + expect(result.content[0].annotations.priority).toBe(0.7); + expect(result.content[0].annotations.audience).toEqual(['user']); + }); + + it('should return debug message with low priority', async () => { + const { mockServer, handlers } = createMockServer(); + registerGetAnnotatedMessageTool(mockServer); + + const handler = handlers.get('get-annotated-message')!; + const result = await handler({ messageType: 'debug', includeImage: false }); + + expect(result.content[0].text).toContain('Debug:'); + expect(result.content[0].annotations.priority).toBe(0.3); + expect(result.content[0].annotations.audience).toEqual(['assistant']); + }); + + it('should include annotated image when requested', async () => { + const { mockServer, handlers } = createMockServer(); + registerGetAnnotatedMessageTool(mockServer); + + const handler = handlers.get('get-annotated-message')!; + const result = await handler({ messageType: 'success', includeImage: true }); + + expect(result.content).toHaveLength(2); + expect(result.content[1].type).toBe('image'); + expect(result.content[1].annotations).toEqual({ + priority: 0.5, + audience: ['user'] + }); + }); }); - it('should handle cancel action', async () => { - const handlers: Map = new Map(); - const mockSendRequest = vi.fn().mockResolvedValue({ - action: 'cancel', - }); - - const mockServer = { - registerTool: vi.fn((name: string, config: any, handler: Function) => { - handlers.set(name, handler); - }), - server: { - getClientCapabilities: vi.fn(() => ({ elicitation: {} })), - }, - } as unknown as McpServer; - - registerTriggerElicitationRequestTool(mockServer); - - const handler = handlers.get('trigger-elicitation-request')!; - const result = await handler({}, { sendRequest: mockSendRequest }); - - expect(result.content[0].text).toContain('⚠️'); - expect(result.content[0].text).toContain('cancelled'); + describe('trigger-long-running-operation', () => { + it('should complete operation and return result', async () => { + const { mockServer, handlers } = createMockServer(); + registerTriggerLongRunningOperationTool(mockServer); + + const handler = handlers.get('trigger-long-running-operation')!; + // Use very short duration for test + const result = await handler({ duration: 0.1, steps: 2 }, { mcpReq: { _meta: {}, id: 'test-123' } }); + + expect(result.content[0].text).toContain('Long running operation completed'); + expect(result.content[0].text).toContain('Duration: 0.1 seconds'); + expect(result.content[0].text).toContain('Steps: 2'); + }, 10000); + + it('should send progress notifications when progressToken provided', async () => { + const { mockServer, handlers } = createMockServer(); + registerTriggerLongRunningOperationTool(mockServer); + + const handler = handlers.get('trigger-long-running-operation')!; + await handler( + { duration: 0.1, steps: 2 }, + { mcpReq: { _meta: { progressToken: 'token-123' }, id: 'test-456' }, sessionId: 'session-1' } + ); + + expect(mockServer.server.notification).toHaveBeenCalledTimes(2); + expect(mockServer.server.notification).toHaveBeenCalledWith( + expect.objectContaining({ + method: 'notifications/progress', + params: expect.objectContaining({ + progressToken: 'token-123' + }) + }), + expect.any(Object) + ); + }, 10000); }); - }); - describe('trigger-url-elicitation', () => { - // The error-path marker is module-level state shared across cases; reset it - // so tests are independent of order and of each other's leftover keys. - beforeEach(() => { - __resetIssuedErrorPathElicitations(); + describe('get-resource-links', () => { + it('should return specified number of resource links', async () => { + const { mockServer, handlers } = createMockServer(); + registerGetResourceLinksTool(mockServer); + + const handler = handlers.get('get-resource-links')!; + const result = await handler({ count: 3 }); + + // 1 intro text + 3 resource links + expect(result.content).toHaveLength(4); + expect(result.content[0].type).toBe('text'); + expect(result.content[0].text).toContain('3 resource links'); + + // Check resource links + for (let i = 1; i < 4; i++) { + expect(result.content[i].type).toBe('resource_link'); + expect(result.content[i].uri).toBeDefined(); + expect(result.content[i].name).toBeDefined(); + } + }); + + it('should alternate between text and blob resources', async () => { + const { mockServer, handlers } = createMockServer(); + registerGetResourceLinksTool(mockServer); + + const handler = handlers.get('get-resource-links')!; + const result = await handler({ count: 4 }); + + // Odd IDs (1, 3) are blob, even IDs (2, 4) are text + expect(result.content[1].name).toContain('Blob'); + expect(result.content[2].name).toContain('Text'); + expect(result.content[3].name).toContain('Blob'); + expect(result.content[4].name).toContain('Text'); + }); + + it('should use default count of 3', async () => { + const { mockServer, handlers } = createMockServer(); + registerGetResourceLinksTool(mockServer); + + const handler = handlers.get('get-resource-links')!; + const result = await handler({}); + + // 1 intro text + 3 resource links (default) + expect(result.content).toHaveLength(4); + }); }); - it('should not register when client does not support URL elicitation', () => { - const handlers: Map = new Map(); - const mockServer = { - registerTool: vi.fn((name: string, config: any, handler: Function) => { - handlers.set(name, handler); - }), - server: { - getClientCapabilities: vi.fn(() => ({ elicitation: { form: {} } })), - }, - } as unknown as McpServer; - - registerTriggerUrlElicitationTool(mockServer); - - expect(mockServer.registerTool).not.toHaveBeenCalled(); - }); - - it('should register when client supports URL elicitation', () => { - const handlers: Map = new Map(); - const mockServer = { - registerTool: vi.fn((name: string, config: any, handler: Function) => { - handlers.set(name, handler); - }), - server: { - getClientCapabilities: vi.fn(() => ({ elicitation: { url: {} } })), - }, - } as unknown as McpServer; + describe('get-resource-reference', () => { + it('should return text resource reference', async () => { + const { mockServer, handlers } = createMockServer(); + registerGetResourceReferenceTool(mockServer); - registerTriggerUrlElicitationTool(mockServer); + const handler = handlers.get('get-resource-reference')!; + const result = await handler({ resourceType: 'Text', resourceId: 1 }); - expect(mockServer.registerTool).toHaveBeenCalledWith( - 'trigger-url-elicitation', - expect.objectContaining({ - title: 'Trigger URL Elicitation Tool', - description: expect.stringContaining('URL elicitation'), - }), - expect.any(Function) - ); - }); - - it('should send URL-mode elicitation request when errorPath is false', async () => { - const handlers: Map = new Map(); - const mockSendRequest = vi.fn().mockResolvedValue({ - action: 'accept', - }); + expect(result.content).toHaveLength(3); + expect(result.content[0].text).toContain('Resource 1'); + expect(result.content[1].type).toBe('resource'); + expect(result.content[1].resource.uri).toContain('text/1'); + expect(result.content[2].text).toContain('URI'); + }); - const mockServer = { - registerTool: vi.fn((name: string, config: any, handler: Function) => { - handlers.set(name, handler); - }), - server: { - getClientCapabilities: vi.fn(() => ({ elicitation: { url: {} } })), - }, - } as unknown as McpServer; + it('should return blob resource reference', async () => { + const { mockServer, handlers } = createMockServer(); + registerGetResourceReferenceTool(mockServer); - registerTriggerUrlElicitationTool(mockServer); + const handler = handlers.get('get-resource-reference')!; + const result = await handler({ resourceType: 'Blob', resourceId: 5 }); - const handler = handlers.get('trigger-url-elicitation')!; - const result = await handler( - { - url: 'https://example.com/verify', - message: 'Open this page to verify your identity', - elicitationId: 'elicitation-123', - errorPath: false, - }, - { sendRequest: mockSendRequest } - ); - - expect(mockSendRequest).toHaveBeenCalledWith( - expect.objectContaining({ - method: 'elicitation/create', - params: expect.objectContaining({ - mode: 'url', - url: 'https://example.com/verify', - message: 'Open this page to verify your identity', - elicitationId: 'elicitation-123', - }), - }), - expect.anything(), - expect.anything() - ); + expect(result.content[1].resource.uri).toContain('blob/5'); + }); - expect(result.content[0].text).toContain( - '✅ User completed the URL elicitation flow.' - ); - }); + it('should reject invalid resource type', async () => { + const { mockServer, handlers } = createMockServer(); + registerGetResourceReferenceTool(mockServer); - it('should not register when client has no elicitation capability at all', () => { - const mockServer = { - registerTool: vi.fn(), - server: { - getClientCapabilities: vi.fn(() => ({})), - }, - } as unknown as McpServer; + const handler = handlers.get('get-resource-reference')!; + await expect(handler({ resourceType: 'Invalid', resourceId: 1 })).rejects.toThrow('Invalid resourceType'); + }); - registerTriggerUrlElicitationTool(mockServer); + it('should reject invalid resource ID', async () => { + const { mockServer, handlers } = createMockServer(); + registerGetResourceReferenceTool(mockServer); - expect(mockServer.registerTool).not.toHaveBeenCalled(); + const handler = handlers.get('get-resource-reference')!; + await expect(handler({ resourceType: 'Text', resourceId: -1 })).rejects.toThrow('Invalid resourceId'); + await expect(handler({ resourceType: 'Text', resourceId: 0 })).rejects.toThrow('Invalid resourceId'); + await expect(handler({ resourceType: 'Text', resourceId: 1.5 })).rejects.toThrow('Invalid resourceId'); + }); }); - it('should not register when client capabilities are undefined', () => { - const mockServer = { - registerTool: vi.fn(), - server: { - getClientCapabilities: vi.fn(() => undefined), - }, - } as unknown as McpServer; + describe('toggle-simulated-logging', () => { + it('should start logging when not active', async () => { + const { mockServer, handlers } = createMockServer(); + registerToggleSimulatedLoggingTool(mockServer); - registerTriggerUrlElicitationTool(mockServer); + const handler = handlers.get('toggle-simulated-logging')!; + const result = await handler({}, { sessionId: 'test-session-1' }); - expect(mockServer.registerTool).not.toHaveBeenCalled(); - }); - - it('should default the elicitationId to a random UUID when omitted', async () => { - const handlers: Map = new Map(); - const mockSendRequest = vi.fn().mockResolvedValue({ - action: 'accept', - }); - - const mockServer = { - registerTool: vi.fn((name: string, config: any, handler: Function) => { - handlers.set(name, handler); - }), - server: { - getClientCapabilities: vi.fn(() => ({ elicitation: { url: {} } })), - }, - } as unknown as McpServer; + expect(result.content[0].text).toContain('Started'); + expect(result.content[0].text).toContain('test-session-1'); + }); - registerTriggerUrlElicitationTool(mockServer); + it('should stop logging when already active', async () => { + const { mockServer, handlers } = createMockServer(); + registerToggleSimulatedLoggingTool(mockServer); - const handler = handlers.get('trigger-url-elicitation')!; - await handler( - { - url: 'https://example.com/verify', - message: 'Open this page to verify your identity', - errorPath: false, - }, - { sendRequest: mockSendRequest } - ); + const handler = handlers.get('toggle-simulated-logging')!; - const sentParams = mockSendRequest.mock.calls[0][0].params; - expect(sentParams.elicitationId).toMatch( - /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/ - ); - }); + // First call starts logging + await handler({}, { sessionId: 'test-session-2' }); - it('should report a declined URL elicitation', async () => { - const handlers: Map = new Map(); - const mockSendRequest = vi.fn().mockResolvedValue({ action: 'decline' }); + // Second call stops logging + const result = await handler({}, { sessionId: 'test-session-2' }); - const mockServer = { - registerTool: vi.fn((name: string, config: any, handler: Function) => { - handlers.set(name, handler); - }), - server: { - getClientCapabilities: vi.fn(() => ({ elicitation: { url: {} } })), - }, - } as unknown as McpServer; + expect(result.content[0].text).toContain('Stopped'); + expect(result.content[0].text).toContain('test-session-2'); + }); - registerTriggerUrlElicitationTool(mockServer); + it('should handle undefined sessionId', async () => { + const { mockServer, handlers } = createMockServer(); + registerToggleSimulatedLoggingTool(mockServer); - const handler = handlers.get('trigger-url-elicitation')!; - const result = await handler( - { - url: 'https://example.com/verify', - message: 'Open this page to verify your identity', - elicitationId: 'elicitation-123', - errorPath: false, - }, - { sendRequest: mockSendRequest } - ); + const handler = handlers.get('toggle-simulated-logging')!; + const result = await handler({}, {}); - expect(result.content[0].text).toContain('❌ User declined to open the URL'); + expect(result.content[0].text).toContain('Started'); + }); }); - it('should report a cancelled URL elicitation', async () => { - const handlers: Map = new Map(); - const mockSendRequest = vi.fn().mockResolvedValue({ action: 'cancel' }); + describe('toggle-subscriber-updates', () => { + it('should start updates when not active', async () => { + const { mockServer, handlers } = createMockServer(); + registerToggleSubscriberUpdatesTool(mockServer); - const mockServer = { - registerTool: vi.fn((name: string, config: any, handler: Function) => { - handlers.set(name, handler); - }), - server: { - getClientCapabilities: vi.fn(() => ({ elicitation: { url: {} } })), - }, - } as unknown as McpServer; + const handler = handlers.get('toggle-subscriber-updates')!; + const result = await handler({}, { sessionId: 'sub-session-1' }); - registerTriggerUrlElicitationTool(mockServer); + expect(result.content[0].text).toContain('Started'); + expect(result.content[0].text).toContain('sub-session-1'); + }); - const handler = handlers.get('trigger-url-elicitation')!; - const result = await handler( - { - url: 'https://example.com/verify', - message: 'Open this page to verify your identity', - elicitationId: 'elicitation-123', - errorPath: false, - }, - { sendRequest: mockSendRequest } - ); + it('should stop updates when already active', async () => { + const { mockServer, handlers } = createMockServer(); + registerToggleSubscriberUpdatesTool(mockServer); - expect(result.content[0].text).toContain( - '⚠️ User cancelled the URL elicitation' - ); - }); + const handler = handlers.get('toggle-subscriber-updates')!; - it('should throw MCP error -32042 with a prerequisite elicitation pointing at a different URL when errorPath is true', async () => { - const handlers: Map = new Map(); - const mockServer = { - registerTool: vi.fn((name: string, config: any, handler: Function) => { - handlers.set(name, handler); - }), - server: { - getClientCapabilities: vi.fn(() => ({ elicitation: { url: {} } })), - }, - } as unknown as McpServer; - - registerTriggerUrlElicitationTool(mockServer); - - const handler = handlers.get('trigger-url-elicitation')!; - - expect.assertions(5); - - try { - await handler( - { - url: 'https://example.com/connect', - message: 'Authorization is required to continue.', - elicitationId: 'elicitation-xyz', - errorPath: true, - }, - {} - ); - } catch (error: any) { - expect(error.code).toBe(-32042); - const prerequisite = error.data.elicitations[0]; - expect(prerequisite.mode).toBe('url'); - // The prerequisite must NOT reuse the failing URL, otherwise the client - // would complete it, retry, hit the same error, and loop forever. - expect(prerequisite.url).toBe('https://modelcontextprotocol.io'); - expect(prerequisite.url).not.toBe('https://example.com/connect'); - // It carries its own elicitation id for the prerequisite itself. - expect(typeof prerequisite.elicitationId).toBe('string'); - } - }); + // First call starts updates + await handler({}, { sessionId: 'sub-session-2' }); - it('should ignore errorPath and take the request path when the same call is retried after the prerequisite', async () => { - const handlers: Map = new Map(); - const mockSendRequest = vi.fn().mockResolvedValue({ action: 'accept' }); + // Second call stops updates + const result = await handler({}, { sessionId: 'sub-session-2' }); - const mockServer = { - registerTool: vi.fn((name: string, config: any, handler: Function) => { - handlers.set(name, handler); - }), - server: { - getClientCapabilities: vi.fn(() => ({ elicitation: { url: {} } })), - }, - } as unknown as McpServer; - - registerTriggerUrlElicitationTool(mockServer); - - const handler = handlers.get('trigger-url-elicitation')!; - // A real client retries with the *same* arguments and does not echo the - // prerequisite's elicitationId. Note these args omit elicitationId, so the - // correlation must rely on stable inputs (session + url), not a per-call - // random id. - const args = { - url: 'https://example.com/connect', - message: 'Authorization is required to continue.', - errorPath: true, - }; - const extra = { sessionId: 'session-1', sendRequest: mockSendRequest }; - - // First call: error path issues the prerequisite and throws -32042. - let prerequisiteUrl: string | undefined; - try { - await handler(args, extra); - throw new Error('expected first call to throw'); - } catch (error: any) { - expect(error.code).toBe(-32042); - prerequisiteUrl = error.data.elicitations[0].url; - expect(prerequisiteUrl).toBe('https://modelcontextprotocol.io'); - expect(mockSendRequest).not.toHaveBeenCalled(); - } - - // Plain retry with identical arguments: errorPath is ignored and the call - // proceeds via the request path instead of throwing the prerequisite again. - const result = await handler({ ...args }, extra); - - expect(mockSendRequest).toHaveBeenCalledWith( - expect.objectContaining({ - method: 'elicitation/create', - params: expect.objectContaining({ - mode: 'url', - url: 'https://example.com/connect', - }), - }), - expect.anything(), - expect.anything() - ); - expect(result.content[0].text).toContain( - '✅ User completed the URL elicitation flow.' - ); + expect(result.content[0].text).toContain('Stopped'); + expect(result.content[0].text).toContain('sub-session-2'); + }); }); - }); - describe('get-roots-list', () => { - it('should not register when client does not support roots', () => { - const { mockServer } = createMockServer(); - registerGetRootsListTool(mockServer); - - expect(mockServer.registerTool).not.toHaveBeenCalled(); + describe('trigger-sampling-request', () => { + it('should not register when client does not support sampling', () => { + const { mockServer } = createMockServer(); + registerTriggerSamplingRequestTool(mockServer); + + // Tool should not be registered since mock server returns empty capabilities + expect(mockServer.registerTool).not.toHaveBeenCalled(); + }); + + it('should register when client supports sampling', () => { + const handlers: Map = new Map(); + const mockServer = { + registerTool: vi.fn((name: string, config: any, handler: Function) => { + handlers.set(name, handler); + }), + server: { + getClientCapabilities: vi.fn(() => ({ sampling: {} })) + } + } as unknown as McpServer; + + registerTriggerSamplingRequestTool(mockServer); + + expect(mockServer.registerTool).toHaveBeenCalledWith( + 'trigger-sampling-request', + expect.objectContaining({ + title: 'Trigger Sampling Request Tool', + description: expect.stringContaining('Sampling') + }), + expect.any(Function) + ); + }); + + it('should send sampling request and return result', async () => { + const handlers: Map = new Map(); + const mockSendRequest = vi.fn().mockResolvedValue({ + model: 'test-model', + content: { type: 'text', text: 'LLM response' } + }); + + const mockServer = { + registerTool: vi.fn((name: string, config: any, handler: Function) => { + handlers.set(name, handler); + }), + server: { + getClientCapabilities: vi.fn(() => ({ sampling: {} })) + } + } as unknown as McpServer; + + registerTriggerSamplingRequestTool(mockServer); + + const handler = handlers.get('trigger-sampling-request')!; + const result = await handler({ prompt: 'Test prompt', maxTokens: 50 }, { mcpReq: { send: mockSendRequest } }); + + expect(mockSendRequest).toHaveBeenCalledWith( + expect.objectContaining({ + method: 'sampling/createMessage', + params: expect.objectContaining({ + maxTokens: 50 + }) + }), + expect.anything() + ); + expect(result.content[0].text).toContain('LLM sampling result'); + }); }); - it('should register when client supports roots', () => { - const handlers: Map = new Map(); - const mockServer = { - registerTool: vi.fn((name: string, config: any, handler: Function) => { - handlers.set(name, handler); - }), - server: { - getClientCapabilities: vi.fn(() => ({ roots: {} })), - }, - } as unknown as McpServer; - - registerGetRootsListTool(mockServer); - - expect(mockServer.registerTool).toHaveBeenCalledWith( - 'get-roots-list', - expect.objectContaining({ - title: 'Get Roots List Tool', - description: expect.stringContaining('roots'), - }), - expect.any(Function) - ); - }); - }); - - describe('simulate-research-query', () => { - function createMockServerWithTasks() { - const taskHandlers: Record = {}; - const mockServer = { - experimental: { - tasks: { - registerToolTask: vi.fn((_name: string, _config: any, handler: any) => { - Object.assign(taskHandlers, handler); - }), - }, - }, - server: { getClientCapabilities: vi.fn(() => ({ elicitation: {} })) }, - } as unknown as McpServer; - return { mockServer, taskHandlers }; - } - - function createMockTaskStore(taskId: string) { - return { - createTask: vi.fn().mockResolvedValue({ - taskId, - status: 'working', - createdAt: new Date().toISOString(), - lastUpdatedAt: new Date().toISOString(), - ttl: 300000, - pollInterval: 1000, - }), - updateTaskStatus: vi.fn().mockResolvedValue(undefined), - storeTaskResult: vi.fn().mockResolvedValue(undefined), - getTask: vi.fn(), - getTaskResult: vi.fn(), - }; - } - - it('should pass relatedTask to sendRequest when elicitation is triggered', async () => { - vi.useFakeTimers(); - - const { mockServer, taskHandlers } = createMockServerWithTasks(); - registerSimulateResearchQueryTool(mockServer); - - const mockTaskStore = createMockTaskStore('task-abc'); - const mockSendRequest = vi.fn().mockResolvedValue({ - action: 'accept', - content: { interpretation: 'technical' }, - }); - - await taskHandlers.createTask( - { topic: 'python', ambiguous: true }, - { taskStore: mockTaskStore, sendRequest: mockSendRequest } - ); - - await vi.runAllTimersAsync(); - vi.useRealTimers(); - - expect(mockSendRequest).toHaveBeenCalledWith( - expect.objectContaining({ method: 'elicitation/create' }), - expect.anything(), - expect.objectContaining({ relatedTask: { taskId: 'task-abc' } }) - ); - }); - - it('should complete without elicitation for non-ambiguous query', async () => { - vi.useFakeTimers(); - - const { mockServer, taskHandlers } = createMockServerWithTasks(); - registerSimulateResearchQueryTool(mockServer); - - const mockTaskStore = createMockTaskStore('task-def'); - const mockSendRequest = vi.fn(); - - await taskHandlers.createTask( - { topic: 'python', ambiguous: false }, - { taskStore: mockTaskStore, sendRequest: mockSendRequest } - ); - - await vi.runAllTimersAsync(); - vi.useRealTimers(); - - expect(mockSendRequest).not.toHaveBeenCalled(); - expect(mockTaskStore.storeTaskResult).toHaveBeenCalledWith( - 'task-def', 'completed', expect.anything() - ); + describe('trigger-elicitation-request', () => { + it('should not register when client does not support elicitation', () => { + const { mockServer } = createMockServer(); + registerTriggerElicitationRequestTool(mockServer); + + expect(mockServer.registerTool).not.toHaveBeenCalled(); + }); + + it('should register when client supports elicitation', () => { + const handlers: Map = new Map(); + const mockServer = { + registerTool: vi.fn((name: string, config: any, handler: Function) => { + handlers.set(name, handler); + }), + server: { + getClientCapabilities: vi.fn(() => ({ elicitation: {} })) + } + } as unknown as McpServer; + + registerTriggerElicitationRequestTool(mockServer); + + expect(mockServer.registerTool).toHaveBeenCalledWith( + 'trigger-elicitation-request', + expect.objectContaining({ + title: 'Trigger Elicitation Request Tool', + description: expect.stringContaining('Elicitation') + }), + expect.any(Function) + ); + }); + + it('should handle accept action with user content', async () => { + const handlers: Map = new Map(); + const mockSendRequest = vi.fn().mockResolvedValue({ + action: 'accept', + content: { + name: 'John Doe', + check: true, + email: 'john@example.com' + } + }); + + const mockServer = { + registerTool: vi.fn((name: string, config: any, handler: Function) => { + handlers.set(name, handler); + }), + server: { + getClientCapabilities: vi.fn(() => ({ elicitation: {} })) + } + } as unknown as McpServer; + + registerTriggerElicitationRequestTool(mockServer); + + const handler = handlers.get('trigger-elicitation-request')!; + const result = await handler({}, { mcpReq: { send: mockSendRequest } }); + + expect(result.content[0].text).toContain('✅'); + expect(result.content[0].text).toContain('provided'); + expect(result.content[1].text).toContain('John Doe'); + }); + + it('should handle decline action', async () => { + const handlers: Map = new Map(); + const mockSendRequest = vi.fn().mockResolvedValue({ + action: 'decline' + }); + + const mockServer = { + registerTool: vi.fn((name: string, config: any, handler: Function) => { + handlers.set(name, handler); + }), + server: { + getClientCapabilities: vi.fn(() => ({ elicitation: {} })) + } + } as unknown as McpServer; + + registerTriggerElicitationRequestTool(mockServer); + + const handler = handlers.get('trigger-elicitation-request')!; + const result = await handler({}, { mcpReq: { send: mockSendRequest } }); + + expect(result.content[0].text).toContain('❌'); + expect(result.content[0].text).toContain('declined'); + }); + + it('should handle cancel action', async () => { + const handlers: Map = new Map(); + const mockSendRequest = vi.fn().mockResolvedValue({ + action: 'cancel' + }); + + const mockServer = { + registerTool: vi.fn((name: string, config: any, handler: Function) => { + handlers.set(name, handler); + }), + server: { + getClientCapabilities: vi.fn(() => ({ elicitation: {} })) + } + } as unknown as McpServer; + + registerTriggerElicitationRequestTool(mockServer); + + const handler = handlers.get('trigger-elicitation-request')!; + const result = await handler({}, { mcpReq: { send: mockSendRequest } }); + + expect(result.content[0].text).toContain('⚠️'); + expect(result.content[0].text).toContain('cancelled'); + }); }); - }); - - describe('gzip-file-as-resource', () => { - it('should compress data URI and return resource link', async () => { - const registeredResources: any[] = []; - const mockServer = { - registerTool: vi.fn(), - registerResource: vi.fn((...args) => { - registeredResources.push(args); - }), - } as unknown as McpServer; - - // Get the handler - let handler: Function | null = null; - (mockServer.registerTool as any).mockImplementation( - (name: string, config: any, h: Function) => { - handler = h; - } - ); - - registerGZipFileAsResourceTool(mockServer); - // Create a data URI with test content - const testContent = 'Hello, World!'; - const dataUri = `data:text/plain;base64,${Buffer.from(testContent).toString('base64')}`; - - const result = await handler!( - { name: 'test.txt.gz', data: dataUri, outputType: 'resourceLink' } - ); - - expect(result.content[0].type).toBe('resource_link'); - expect(result.content[0].uri).toContain('test.txt.gz'); + describe('trigger-url-elicitation', () => { + // The error-path marker is module-level state shared across cases; reset it + // so tests are independent of order and of each other's leftover keys. + beforeEach(() => { + __resetIssuedErrorPathElicitations(); + }); + + it('should not register when client does not support URL elicitation', () => { + const handlers: Map = new Map(); + const mockServer = { + registerTool: vi.fn((name: string, config: any, handler: Function) => { + handlers.set(name, handler); + }), + server: { + getClientCapabilities: vi.fn(() => ({ elicitation: { form: {} } })) + } + } as unknown as McpServer; + + registerTriggerUrlElicitationTool(mockServer); + + expect(mockServer.registerTool).not.toHaveBeenCalled(); + }); + + it('should register when client supports URL elicitation', () => { + const handlers: Map = new Map(); + const mockServer = { + registerTool: vi.fn((name: string, config: any, handler: Function) => { + handlers.set(name, handler); + }), + server: { + getClientCapabilities: vi.fn(() => ({ elicitation: { url: {} } })) + } + } as unknown as McpServer; + + registerTriggerUrlElicitationTool(mockServer); + + expect(mockServer.registerTool).toHaveBeenCalledWith( + 'trigger-url-elicitation', + expect.objectContaining({ + title: 'Trigger URL Elicitation Tool', + description: expect.stringContaining('URL elicitation') + }), + expect.any(Function) + ); + }); + + it('should send URL-mode elicitation request when errorPath is false', async () => { + const handlers: Map = new Map(); + const mockSendRequest = vi.fn().mockResolvedValue({ + action: 'accept' + }); + + const mockServer = { + registerTool: vi.fn((name: string, config: any, handler: Function) => { + handlers.set(name, handler); + }), + server: { + getClientCapabilities: vi.fn(() => ({ elicitation: { url: {} } })) + } + } as unknown as McpServer; + + registerTriggerUrlElicitationTool(mockServer); + + const handler = handlers.get('trigger-url-elicitation')!; + const result = await handler( + { + url: 'https://example.com/verify', + message: 'Open this page to verify your identity', + elicitationId: 'elicitation-123', + errorPath: false + }, + { mcpReq: { send: mockSendRequest } } + ); + + expect(mockSendRequest).toHaveBeenCalledWith( + expect.objectContaining({ + method: 'elicitation/create', + params: expect.objectContaining({ + mode: 'url', + url: 'https://example.com/verify', + message: 'Open this page to verify your identity', + elicitationId: 'elicitation-123' + }) + }), + expect.anything(), + expect.anything() + ); + + expect(result.content[0].text).toContain('✅ User completed the URL elicitation flow.'); + }); + + it('should not register when client has no elicitation capability at all', () => { + const mockServer = { + registerTool: vi.fn(), + server: { + getClientCapabilities: vi.fn(() => ({})) + } + } as unknown as McpServer; + + registerTriggerUrlElicitationTool(mockServer); + + expect(mockServer.registerTool).not.toHaveBeenCalled(); + }); + + it('should not register when client capabilities are undefined', () => { + const mockServer = { + registerTool: vi.fn(), + server: { + getClientCapabilities: vi.fn(() => undefined) + } + } as unknown as McpServer; + + registerTriggerUrlElicitationTool(mockServer); + + expect(mockServer.registerTool).not.toHaveBeenCalled(); + }); + + it('should default the elicitationId to a random UUID when omitted', async () => { + const handlers: Map = new Map(); + const mockSendRequest = vi.fn().mockResolvedValue({ + action: 'accept' + }); + + const mockServer = { + registerTool: vi.fn((name: string, config: any, handler: Function) => { + handlers.set(name, handler); + }), + server: { + getClientCapabilities: vi.fn(() => ({ elicitation: { url: {} } })) + } + } as unknown as McpServer; + + registerTriggerUrlElicitationTool(mockServer); + + const handler = handlers.get('trigger-url-elicitation')!; + await handler( + { + url: 'https://example.com/verify', + message: 'Open this page to verify your identity', + errorPath: false + }, + { mcpReq: { send: mockSendRequest } } + ); + + const sentParams = mockSendRequest.mock.calls[0][0].params; + expect(sentParams.elicitationId).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/); + }); + + it('should report a declined URL elicitation', async () => { + const handlers: Map = new Map(); + const mockSendRequest = vi.fn().mockResolvedValue({ action: 'decline' }); + + const mockServer = { + registerTool: vi.fn((name: string, config: any, handler: Function) => { + handlers.set(name, handler); + }), + server: { + getClientCapabilities: vi.fn(() => ({ elicitation: { url: {} } })) + } + } as unknown as McpServer; + + registerTriggerUrlElicitationTool(mockServer); + + const handler = handlers.get('trigger-url-elicitation')!; + const result = await handler( + { + url: 'https://example.com/verify', + message: 'Open this page to verify your identity', + elicitationId: 'elicitation-123', + errorPath: false + }, + { mcpReq: { send: mockSendRequest } } + ); + + expect(result.content[0].text).toContain('❌ User declined to open the URL'); + }); + + it('should report a cancelled URL elicitation', async () => { + const handlers: Map = new Map(); + const mockSendRequest = vi.fn().mockResolvedValue({ action: 'cancel' }); + + const mockServer = { + registerTool: vi.fn((name: string, config: any, handler: Function) => { + handlers.set(name, handler); + }), + server: { + getClientCapabilities: vi.fn(() => ({ elicitation: { url: {} } })) + } + } as unknown as McpServer; + + registerTriggerUrlElicitationTool(mockServer); + + const handler = handlers.get('trigger-url-elicitation')!; + const result = await handler( + { + url: 'https://example.com/verify', + message: 'Open this page to verify your identity', + elicitationId: 'elicitation-123', + errorPath: false + }, + { mcpReq: { send: mockSendRequest } } + ); + + expect(result.content[0].text).toContain('⚠️ User cancelled the URL elicitation'); + }); + + it('should throw MCP error -32042 with a prerequisite elicitation pointing at a different URL when errorPath is true', async () => { + const handlers: Map = new Map(); + const mockServer = { + registerTool: vi.fn((name: string, config: any, handler: Function) => { + handlers.set(name, handler); + }), + server: { + getClientCapabilities: vi.fn(() => ({ elicitation: { url: {} } })) + } + } as unknown as McpServer; + + registerTriggerUrlElicitationTool(mockServer); + + const handler = handlers.get('trigger-url-elicitation')!; + + expect.assertions(5); + + try { + await handler( + { + url: 'https://example.com/connect', + message: 'Authorization is required to continue.', + elicitationId: 'elicitation-xyz', + errorPath: true + }, + {} + ); + } catch (error: any) { + expect(error.code).toBe(-32042); + const prerequisite = error.data.elicitations[0]; + expect(prerequisite.mode).toBe('url'); + // The prerequisite must NOT reuse the failing URL, otherwise the client + // would complete it, retry, hit the same error, and loop forever. + expect(prerequisite.url).toBe('https://modelcontextprotocol.io'); + expect(prerequisite.url).not.toBe('https://example.com/connect'); + // It carries its own elicitation id for the prerequisite itself. + expect(typeof prerequisite.elicitationId).toBe('string'); + } + }); + + it('should ignore errorPath and take the request path when the same call is retried after the prerequisite', async () => { + const handlers: Map = new Map(); + const mockSendRequest = vi.fn().mockResolvedValue({ action: 'accept' }); + + const mockServer = { + registerTool: vi.fn((name: string, config: any, handler: Function) => { + handlers.set(name, handler); + }), + server: { + getClientCapabilities: vi.fn(() => ({ elicitation: { url: {} } })) + } + } as unknown as McpServer; + + registerTriggerUrlElicitationTool(mockServer); + + const handler = handlers.get('trigger-url-elicitation')!; + // A real client retries with the *same* arguments and does not echo the + // prerequisite's elicitationId. Note these args omit elicitationId, so the + // correlation must rely on stable inputs (session + url), not a per-call + // random id. + const args = { + url: 'https://example.com/connect', + message: 'Authorization is required to continue.', + errorPath: true + }; + const extra = { sessionId: 'session-1', mcpReq: { send: mockSendRequest } }; + + // First call: error path issues the prerequisite and throws -32042. + let prerequisiteUrl: string | undefined; + try { + await handler(args, extra); + throw new Error('expected first call to throw'); + } catch (error: any) { + expect(error.code).toBe(-32042); + prerequisiteUrl = error.data.elicitations[0].url; + expect(prerequisiteUrl).toBe('https://modelcontextprotocol.io'); + expect(mockSendRequest).not.toHaveBeenCalled(); + } + + // Plain retry with identical arguments: errorPath is ignored and the call + // proceeds via the request path instead of throwing the prerequisite again. + const result = await handler({ ...args }, extra); + + expect(mockSendRequest).toHaveBeenCalledWith( + expect.objectContaining({ + method: 'elicitation/create', + params: expect.objectContaining({ + mode: 'url', + url: 'https://example.com/connect' + }) + }), + expect.anything(), + expect.anything() + ); + expect(result.content[0].text).toContain('✅ User completed the URL elicitation flow.'); + }); }); - it('should return resource directly when outputType is resource', async () => { - const mockServer = { - registerTool: vi.fn(), - registerResource: vi.fn(), - } as unknown as McpServer; - - let handler: Function | null = null; - (mockServer.registerTool as any).mockImplementation( - (name: string, config: any, h: Function) => { - handler = h; - } - ); - - registerGZipFileAsResourceTool(mockServer); - - const testContent = 'Test content for compression'; - const dataUri = `data:text/plain;base64,${Buffer.from(testContent).toString('base64')}`; - - const result = await handler!( - { name: 'output.gz', data: dataUri, outputType: 'resource' } - ); - - expect(result.content[0].type).toBe('resource'); - expect(result.content[0].resource.mimeType).toBe('application/gzip'); - expect(result.content[0].resource.blob).toBeDefined(); + describe('get-roots-list', () => { + it('should not register when client does not support roots', () => { + const { mockServer } = createMockServer(); + registerGetRootsListTool(mockServer); + + expect(mockServer.registerTool).not.toHaveBeenCalled(); + }); + + it('should register when client supports roots', () => { + const handlers: Map = new Map(); + const mockServer = { + registerTool: vi.fn((name: string, config: any, handler: Function) => { + handlers.set(name, handler); + }), + server: { + getClientCapabilities: vi.fn(() => ({ roots: {} })) + } + } as unknown as McpServer; + + registerGetRootsListTool(mockServer); + + expect(mockServer.registerTool).toHaveBeenCalledWith( + 'get-roots-list', + expect.objectContaining({ + title: 'Get Roots List Tool', + description: expect.stringContaining('roots') + }), + expect.any(Function) + ); + }); }); - it('should reject unsupported URL protocols', async () => { - const mockServer = { - registerTool: vi.fn(), - registerResource: vi.fn(), - } as unknown as McpServer; - - let handler: Function | null = null; - (mockServer.registerTool as any).mockImplementation( - (name: string, config: any, h: Function) => { - handler = h; - } - ); - - registerGZipFileAsResourceTool(mockServer); - - await expect( - handler!({ name: 'test.gz', data: 'ftp://example.com/file.txt', outputType: 'resource' }) - ).rejects.toThrow('Unsupported URL protocol'); + describe('gzip-file-as-resource', () => { + it('should compress data URI and return resource link', async () => { + const registeredResources: any[] = []; + const mockServer = { + registerTool: vi.fn(), + registerResource: vi.fn((...args) => { + registeredResources.push(args); + }) + } as unknown as McpServer; + + // Get the handler + let handler: Function | null = null; + (mockServer.registerTool as any).mockImplementation((name: string, config: any, h: Function) => { + handler = h; + }); + + registerGZipFileAsResourceTool(mockServer); + + // Create a data URI with test content + const testContent = 'Hello, World!'; + const dataUri = `data:text/plain;base64,${Buffer.from(testContent).toString('base64')}`; + + const result = await handler!({ name: 'test.txt.gz', data: dataUri, outputType: 'resourceLink' }); + + expect(result.content[0].type).toBe('resource_link'); + expect(result.content[0].uri).toContain('test.txt.gz'); + }); + + it('should return resource directly when outputType is resource', async () => { + const mockServer = { + registerTool: vi.fn(), + registerResource: vi.fn() + } as unknown as McpServer; + + let handler: Function | null = null; + (mockServer.registerTool as any).mockImplementation((name: string, config: any, h: Function) => { + handler = h; + }); + + registerGZipFileAsResourceTool(mockServer); + + const testContent = 'Test content for compression'; + const dataUri = `data:text/plain;base64,${Buffer.from(testContent).toString('base64')}`; + + const result = await handler!({ name: 'output.gz', data: dataUri, outputType: 'resource' }); + + expect(result.content[0].type).toBe('resource'); + expect(result.content[0].resource.mimeType).toBe('application/gzip'); + expect(result.content[0].resource.blob).toBeDefined(); + }); + + it('should reject unsupported URL protocols', async () => { + const mockServer = { + registerTool: vi.fn(), + registerResource: vi.fn() + } as unknown as McpServer; + + let handler: Function | null = null; + (mockServer.registerTool as any).mockImplementation((name: string, config: any, h: Function) => { + handler = h; + }); + + registerGZipFileAsResourceTool(mockServer); + + await expect(handler!({ name: 'test.gz', data: 'ftp://example.com/file.txt', outputType: 'resource' })).rejects.toThrow( + 'Unsupported URL protocol' + ); + }); }); - }); }); diff --git a/src/everything/docs/architecture.md b/src/everything/docs/architecture.md index 728cfd4010..07f4ec2864 100644 --- a/src/everything/docs/architecture.md +++ b/src/everything/docs/architecture.md @@ -29,9 +29,9 @@ resource subscriptions and simulated logging. ## Build and Distribution -- TypeScript sources are compiled into `dist/` via `npm run build`. -- The `build` script copies `docs/` into `dist/` so instruction files ship alongside the compiled server. -- The CLI bin is configured in `package.json` as `mcp-server-everything` → `dist/index.js`. +- TypeScript sources are compiled into `dist/` via `npm run build`. +- The `build` script copies `docs/` into `dist/` so instruction files ship alongside the compiled server. +- The CLI bin is configured in `package.json` as `mcp-server-everything` → `dist/index.js`. ## [Project Structure](structure.md) diff --git a/src/everything/docs/extension.md b/src/everything/docs/extension.md index 1d77730448..206026b0f8 100644 --- a/src/everything/docs/extension.md +++ b/src/everything/docs/extension.md @@ -9,15 +9,15 @@ ## Adding Tools -- Create a new file under `tools/` with your `registerXTool(server)` function that registers the tool via `server.registerTool(...)`. -- Export and call it from `tools/index.ts` inside `registerTools(server)`. +- Create a new file under `tools/` with your `registerXTool(server)` function that registers the tool via `server.registerTool(...)`. +- Export and call it from `tools/index.ts` inside `registerTools(server)`. ## Adding Prompts -- Create a new file under `prompts/` with your `registerXPrompt(server)` function that registers the prompt via `server.registerPrompt(...)`. -- Export and call it from `prompts/index.ts` inside `registerPrompts(server)`. +- Create a new file under `prompts/` with your `registerXPrompt(server)` function that registers the prompt via `server.registerPrompt(...)`. +- Export and call it from `prompts/index.ts` inside `registerPrompts(server)`. ## Adding Resources -- Create a new file under `resources/` with your `registerXResources(server)` function using `server.registerResource(...)` (optionally with `ResourceTemplate`). -- Export and call it from `resources/index.ts` inside `registerResources(server)`. +- Create a new file under `resources/` with your `registerXResources(server)` function using `server.registerResource(...)` (optionally with `ResourceTemplate`). +- Export and call it from `resources/index.ts` inside `registerResources(server)`. diff --git a/src/everything/docs/features.md b/src/everything/docs/features.md index a5429ac600..b9b5d057c6 100644 --- a/src/everything/docs/features.md +++ b/src/everything/docs/features.md @@ -9,60 +9,60 @@ ## Tools -- `echo` (tools/echo.ts): Echoes the provided `message: string`. Uses Zod to validate inputs. -- `get-annotated-message` (tools/get-annotated-message.ts): Returns a `text` message annotated with `priority` and `audience` based on `messageType` (`error`, `success`, or `debug`); can optionally include an annotated `image`. -- `get-env` (tools/get-env.ts): Returns all environment variables from the running process as pretty-printed JSON text. -- `get-resource-links` (tools/get-resource-links.ts): Returns an intro `text` block followed by multiple `resource_link` items. For a requested `count` (1–10), alternates between dynamic Text and Blob resources using URIs from `resources/templates.ts`. -- `get-resource-reference` (tools/get-resource-reference.ts): Accepts `resourceType` (`text` or `blob`) and `resourceId` (positive integer). Returns a concrete `resource` content block (with its `uri`, `mimeType`, and data) with surrounding explanatory `text`. -- `get-roots-list` (tools/get-roots-list.ts): Returns the last list of roots sent by the client. -- `gzip-file-as-resource` (tools/gzip-file-as-resource.ts): Accepts a `name` and `data` (URL or data URI), fetches the data subject to size/time/domain constraints, compresses it, registers it as a session resource at `demo://resource/session/` with `mimeType: application/gzip`, and returns either a `resource_link` (default) or an inline `resource` depending on `outputType`. -- `get-structured-content` (tools/get-structured-content.ts): Demonstrates structured responses. Accepts `location` input and returns both backward‑compatible `content` (a `text` block containing JSON) and `structuredContent` validated by an `outputSchema` (temperature, conditions, humidity). -- `get-sum` (tools/get-sum.ts): For two numbers `a` and `b` calculates and returns their sum. Uses Zod to validate inputs. -- `get-tiny-image` (tools/get-tiny-image.ts): Returns a tiny PNG MCP logo as an `image` content item with brief descriptive text before and after. -- `trigger-long-running-operation` (tools/trigger-long-running-operation.ts): Simulates a multi-step operation over a given `duration` and number of `steps`; reports progress via `notifications/progress` when a `progressToken` is provided by the client. -- `toggle-simulated-logging` (tools/toggle-simulated-logging.ts): Starts or stops simulated, random‑leveled logging for the invoking session. Respects the client’s selected minimum logging level. -- `toggle-subscriber-updates` (tools/toggle-subscriber-updates.ts): Starts or stops simulated resource update notifications for URIs the invoking session has subscribed to. -- `trigger-elicitation-request` (tools/trigger-elicitation-request.ts): Issues an `elicitation/create` request using form-mode fields (strings, numbers, booleans, enums, and format validation) and returns the resulting action/content. -- `trigger-url-elicitation` (tools/trigger-url-elicitation.ts): Issues an `elicitation/create` request in URL mode (`mode: "url"`) with an `elicitationId`, or throws MCP error `-32042` (`UrlElicitationRequiredError`) when `errorPath=true`. On the error path the prerequisite elicitation it returns points at a different URL than the failing request (`https://modelcontextprotocol.io`); when the client satisfies it and retries the same call, the retry ignores `errorPath` and proceeds via the request path, so the client does not loop on the same error. The retry marker is one-shot per `(session, url, elicitationId)`: it is cleared on the recognized retry, so re-running the error path with identical arguments without an intervening prerequisite is treated as a retry and proceeds. Requires client capability `elicitation.url`. -- `trigger-sampling-request` (tools/trigger-sampling-request.ts): Issues a `sampling/createMessage` request to the client/LLM using provided `prompt` and optional generation controls; returns the LLM's response payload. -- `simulate-research-query` (tools/simulate-research-query.ts): Demonstrates MCP Tasks (SEP-1686) with a simulated multi-stage research operation. Accepts `topic` and `ambiguous` parameters. Returns a task that progresses through stages with status updates. If `ambiguous` is true and client supports elicitation, sends an elicitation request directly to gather clarification before completing. -- `trigger-sampling-request-async` (tools/trigger-sampling-request-async.ts): Demonstrates bidirectional tasks where the server sends a sampling request that the client executes as a background task. Server polls for status and retrieves the LLM result when complete. Requires client to support `tasks.requests.sampling.createMessage`. -- `trigger-elicitation-request-async` (tools/trigger-elicitation-request-async.ts): Demonstrates bidirectional tasks where the server sends an elicitation request that the client executes as a background task. Server polls while waiting for user input. Requires client to support `tasks.requests.elicitation.create`. +- `echo` (tools/echo.ts): Echoes the provided `message: string`. Uses Zod to validate inputs. +- `get-annotated-message` (tools/get-annotated-message.ts): Returns a `text` message annotated with `priority` and `audience` based on `messageType` (`error`, `success`, or `debug`); can optionally include an annotated `image`. +- `get-env` (tools/get-env.ts): Returns all environment variables from the running process as pretty-printed JSON text. +- `get-resource-links` (tools/get-resource-links.ts): Returns an intro `text` block followed by multiple `resource_link` items. For a requested `count` (1–10), alternates between dynamic Text and Blob resources using URIs from `resources/templates.ts`. +- `get-resource-reference` (tools/get-resource-reference.ts): Accepts `resourceType` (`text` or `blob`) and `resourceId` (positive integer). Returns a concrete `resource` content block (with its `uri`, `mimeType`, and data) with surrounding explanatory `text`. +- `get-roots-list` (tools/get-roots-list.ts): Returns the last list of roots sent by the client. +- `gzip-file-as-resource` (tools/gzip-file-as-resource.ts): Accepts a `name` and `data` (URL or data URI), fetches the data subject to size/time/domain constraints, compresses it, registers it as a session resource at `demo://resource/session/` with `mimeType: application/gzip`, and returns either a `resource_link` (default) or an inline `resource` depending on `outputType`. +- `get-structured-content` (tools/get-structured-content.ts): Demonstrates structured responses. Accepts `location` input and returns both backward‑compatible `content` (a `text` block containing JSON) and `structuredContent` validated by an `outputSchema` (temperature, conditions, humidity). +- `get-sum` (tools/get-sum.ts): For two numbers `a` and `b` calculates and returns their sum. Uses Zod to validate inputs. +- `get-tiny-image` (tools/get-tiny-image.ts): Returns a tiny PNG MCP logo as an `image` content item with brief descriptive text before and after. +- `trigger-long-running-operation` (tools/trigger-long-running-operation.ts): Simulates a multi-step operation over a given `duration` and number of `steps`; reports progress via `notifications/progress` when a `progressToken` is provided by the client. +- `toggle-simulated-logging` (tools/toggle-simulated-logging.ts): Starts or stops simulated, random‑leveled logging for the invoking session. Respects the client’s selected minimum logging level. +- `toggle-subscriber-updates` (tools/toggle-subscriber-updates.ts): Starts or stops simulated resource update notifications for URIs the invoking session has subscribed to. +- `trigger-elicitation-request` (tools/trigger-elicitation-request.ts): Issues an `elicitation/create` request using form-mode fields (strings, numbers, booleans, enums, and format validation) and returns the resulting action/content. +- `trigger-url-elicitation` (tools/trigger-url-elicitation.ts): Issues an `elicitation/create` request in URL mode (`mode: "url"`) with an `elicitationId`, or throws MCP error `-32042` (`UrlElicitationRequiredError`) when `errorPath=true`. On the error path the prerequisite elicitation it returns points at a different URL than the failing request (`https://modelcontextprotocol.io`); when the client satisfies it and retries the same call, the retry ignores `errorPath` and proceeds via the request path, so the client does not loop on the same error. The retry marker is one-shot per `(session, url, elicitationId)`: it is cleared on the recognized retry, so re-running the error path with identical arguments without an intervening prerequisite is treated as a retry and proceeds. Requires client capability `elicitation.url`. +- `trigger-sampling-request` (tools/trigger-sampling-request.ts): Issues a `sampling/createMessage` request to the client/LLM using provided `prompt` and optional generation controls; returns the LLM's response payload. +- `simulate-research-query` (tools/simulate-research-query.ts): Demonstrates MCP Tasks (SEP-1686) with a simulated multi-stage research operation. Accepts `topic` and `ambiguous` parameters. Returns a task that progresses through stages with status updates. If `ambiguous` is true and client supports elicitation, sends an elicitation request directly to gather clarification before completing. +- `trigger-sampling-request-async` (tools/trigger-sampling-request-async.ts): Demonstrates bidirectional tasks where the server sends a sampling request that the client executes as a background task. Server polls for status and retrieves the LLM result when complete. Requires client to support `tasks.requests.sampling.createMessage`. +- `trigger-elicitation-request-async` (tools/trigger-elicitation-request-async.ts): Demonstrates bidirectional tasks where the server sends an elicitation request that the client executes as a background task. Server polls while waiting for user input. Requires client to support `tasks.requests.elicitation.create`. ## Prompts -- `simple-prompt` (prompts/simple.ts): No-argument prompt that returns a static user message. -- `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 `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`. +- `simple-prompt` (prompts/simple.ts): No-argument prompt that returns a static user message. +- `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 `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 Documents: `demo://resource/static/document/` (serves files from `src/everything/docs/` as static file-based resources) -- Session Scoped: `demo://resource/session/` (per-session resources registered dynamically; available only for the lifetime of the session) +- 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 Documents: `demo://resource/static/document/` (serves files from `src/everything/docs/` as static file-based resources) +- Session Scoped: `demo://resource/session/` (per-session resources registered dynamically; available only for the lifetime of the session) ## Resource Subscriptions and Notifications -- Simulated update notifications are opt‑in and off by default. -- Clients may subscribe/unsubscribe to resource URIs using the MCP `resources/subscribe` and `resources/unsubscribe` requests. -- Use the `toggle-subscriber-updates` tool to start/stop a per‑session interval that emits `notifications/resources/updated { uri }` only for URIs that session has subscribed to. -- Multiple concurrent clients are supported; each client’s subscriptions are tracked per session and notifications are delivered independently via the server instance associated with that session. +- Simulated update notifications are opt‑in and off by default. +- Clients may subscribe/unsubscribe to resource URIs using the MCP `resources/subscribe` and `resources/unsubscribe` requests. +- Use the `toggle-subscriber-updates` tool to start/stop a per‑session interval that emits `notifications/resources/updated { uri }` only for URIs that session has subscribed to. +- Multiple concurrent clients are supported; each client’s subscriptions are tracked per session and notifications are delivered independently via the server instance associated with that session. ## Simulated Logging -- Simulated logging is available but off by default. -- Use the `toggle-simulated-logging` tool to start/stop periodic log messages of varying levels (debug, info, notice, warning, error, critical, alert, emergency) per session. -- Clients can control the minimum level they receive via the standard MCP `logging/setLevel` request. +- Simulated logging is available but off by default. +- Use the `toggle-simulated-logging` tool to start/stop periodic log messages of varying levels (debug, info, notice, warning, error, critical, alert, emergency) per session. +- Clients can control the minimum level they receive via the standard MCP `logging/setLevel` request. ## Tasks (SEP-1686) The server advertises support for MCP Tasks, enabling long-running operations with status tracking: -- **Capabilities advertised**: `tasks.list`, `tasks.cancel`, `tasks.requests.tools.call` -- **Task Store**: Uses `InMemoryTaskStore` from SDK experimental for task lifecycle management -- **Message Queue**: Uses `InMemoryTaskMessageQueue` for task-related messaging +- **Capabilities advertised**: `tasks.list`, `tasks.cancel`, `tasks.requests.tools.call` +- **Task Store**: Uses `InMemoryTaskStore` from SDK experimental for task lifecycle management +- **Message Queue**: Uses `InMemoryTaskMessageQueue` for task-related messaging ### Task Lifecycle @@ -73,11 +73,11 @@ The server advertises support for MCP Tasks, enabling long-running operations wi ### Task Statuses -- `working`: Task is actively processing -- `input_required`: Task needs additional input (server sends elicitation request directly) -- `completed`: Task finished successfully -- `failed`: Task encountered an error -- `cancelled`: Task was cancelled by client +- `working`: Task is actively processing +- `input_required`: Task needs additional input (server sends elicitation request directly) +- `completed`: Task finished successfully +- `failed`: Task encountered an error +- `cancelled`: Task was cancelled by client ### Demo Tools diff --git a/src/everything/docs/how-it-works.md b/src/everything/docs/how-it-works.md index 514c6f5663..af77cde7ff 100644 --- a/src/everything/docs/how-it-works.md +++ b/src/everything/docs/how-it-works.md @@ -11,35 +11,35 @@ ### Module: `server/index.ts` -- Some tools require client support for the capability they demonstrate. These are: - - `get-roots-list` - - `trigger-elicitation-request` - - `trigger-sampling-request` -- Client capabilities aren't known until after initilization handshake is complete. -- Most tools are registered immediately during the Server Factory execution, prior to client connection. -- To defer registration of these commands until client capabilities are known, a `registerConditionalTools(server)` function is invoked from an `onintitialized` handler. +- Some tools require client support for the capability they demonstrate. These are: + - `get-roots-list` + - `trigger-elicitation-request` + - `trigger-sampling-request` +- Client capabilities aren't known until after initilization handshake is complete. +- Most tools are registered immediately during the Server Factory execution, prior to client connection. +- To defer registration of these commands until client capabilities are known, a `registerConditionalTools(server)` function is invoked from an `onintitialized` handler. ## Resource Subscriptions ### Module: `resources/subscriptions.ts` -- Tracks subscribers per URI: `Map>`. -- Installs handlers via `setSubscriptionHandlers(server)` to process subscribe/unsubscribe requests and keep the map updated. -- Updates are started/stopped on demand by the `toggle-subscriber-updates` tool, which calls `beginSimulatedResourceUpdates(server, sessionId)` and `stopSimulatedResourceUpdates(sessionId)`. -- `cleanup(sessionId?)` calls `stopSimulatedResourceUpdates(sessionId)` to clear intervals and remove session‑scoped state. +- Tracks subscribers per URI: `Map>`. +- Installs handlers via `setSubscriptionHandlers(server)` to process subscribe/unsubscribe requests and keep the map updated. +- Updates are started/stopped on demand by the `toggle-subscriber-updates` tool, which calls `beginSimulatedResourceUpdates(server, sessionId)` and `stopSimulatedResourceUpdates(sessionId)`. +- `cleanup(sessionId?)` calls `stopSimulatedResourceUpdates(sessionId)` to clear intervals and remove session‑scoped state. ## Session‑scoped Resources ### Module: `resources/session.ts` -- `getSessionResourceURI(name: string)`: Builds a session resource URI: `demo://resource/session/`. -- `registerSessionResource(server, resource, type, payload)`: Registers a resource with the given `uri`, `name`, and `mimeType`, returning a `resource_link`. The content is served from memory for the life of the session only. Supports `type: "text" | "blob"` and returns data in the corresponding field. -- Intended usage: tools can create and expose per-session artifacts without persisting them. For example, `tools/gzip-file-as-resource.ts` compresses fetched content, registers it as a session resource with `mimeType: application/gzip`, and returns either a `resource_link` or an inline `resource` based on `outputType`. +- `getSessionResourceURI(name: string)`: Builds a session resource URI: `demo://resource/session/`. +- `registerSessionResource(server, resource, type, payload)`: Registers a resource with the given `uri`, `name`, and `mimeType`, returning a `resource_link`. The content is served from memory for the life of the session only. Supports `type: "text" | "blob"` and returns data in the corresponding field. +- Intended usage: tools can create and expose per-session artifacts without persisting them. For example, `tools/gzip-file-as-resource.ts` compresses fetched content, registers it as a session resource with `mimeType: application/gzip`, and returns either a `resource_link` or an inline `resource` based on `outputType`. ## Simulated Logging ### Module: `server/logging.ts` -- Periodically sends randomized log messages at different levels. Messages can include the session ID for clarity during demos. -- Started/stopped on demand via the `toggle-simulated-logging` tool, which calls `beginSimulatedLogging(server, sessionId?)` and `stopSimulatedLogging(sessionId?)`. Note that transport disconnect triggers `cleanup()` which also stops any active intervals. -- Uses `server.sendLoggingMessage({ level, data }, sessionId?)` so that the client’s configured minimum logging level is respected by the SDK. +- Periodically sends randomized log messages at different levels. Messages can include the session ID for clarity during demos. +- Started/stopped on demand via the `toggle-simulated-logging` tool, which calls `beginSimulatedLogging(server, sessionId?)` and `stopSimulatedLogging(sessionId?)`. Note that transport disconnect triggers `cleanup()` which also stops any active intervals. +- Uses `server.sendLoggingMessage({ level, data }, sessionId?)` so that the client’s configured minimum logging level is respected by the SDK. diff --git a/src/everything/docs/instructions.md b/src/everything/docs/instructions.md index 5806dc0ba9..d0f32d5469 100644 --- a/src/everything/docs/instructions.md +++ b/src/everything/docs/instructions.md @@ -5,23 +5,23 @@ Follow them to use, extend, and troubleshoot the server safely and effectively. ## Cross-Feature Relationships -- Use `get-roots-list` to see client workspace roots before file operations -- `gzip-file-as-resource` creates session-scoped resources accessible only during the current session -- Enable `toggle-simulated-logging` before debugging to see server log messages -- Enable `toggle-subscriber-updates` to receive periodic resource update notifications +- Use `get-roots-list` to see client workspace roots before file operations +- `gzip-file-as-resource` creates session-scoped resources accessible only during the current session +- Enable `toggle-simulated-logging` before debugging to see server log messages +- Enable `toggle-subscriber-updates` to receive periodic resource update notifications ## Constraints & Limitations -- `gzip-file-as-resource`: Max fetch size controlled by `GZIP_MAX_FETCH_SIZE` (default 10MB), timeout by `GZIP_MAX_FETCH_TIME_MILLIS` (default 30s), allowed domains by `GZIP_ALLOWED_DOMAINS` -- Session resources are ephemeral and lost when the session ends -- Sampling requests (`trigger-sampling-request`) require client sampling capability -- Elicitation requests (`trigger-elicitation-request`) require client elicitation capability +- `gzip-file-as-resource`: Max fetch size controlled by `GZIP_MAX_FETCH_SIZE` (default 10MB), timeout by `GZIP_MAX_FETCH_TIME_MILLIS` (default 30s), allowed domains by `GZIP_ALLOWED_DOMAINS` +- Session resources are ephemeral and lost when the session ends +- Sampling requests (`trigger-sampling-request`) require client sampling capability +- Elicitation requests (`trigger-elicitation-request`) require client elicitation capability ## Operational Patterns -- For long operations, use `trigger-long-running-operation` which sends progress notifications -- Prefer reading resources before calling mutating tools -- Check `get-roots-list` output to understand the client's workspace context +- For long operations, use `trigger-long-running-operation` which sends progress notifications +- Prefer reading resources before calling mutating tools +- Check `get-roots-list` output to understand the client's workspace context ## Easter Egg diff --git a/src/everything/docs/startup.md b/src/everything/docs/startup.md index 1d006589a9..bd2f02a6ab 100644 --- a/src/everything/docs/startup.md +++ b/src/everything/docs/startup.md @@ -9,63 +9,63 @@ ## 1. Everything Server Launcher -- Usage `node dist/index.js [stdio|sse|streamableHttp]` -- Runs the specified **transport manager** to handle client connections. -- Specify transport type on command line (default `stdio`) - - `stdio` → `transports/stdio.js` - - `sse` → `transports/sse.js` - - `streamableHttp` → `transports/streamableHttp.js` +- Usage `node dist/index.js [stdio|sse|streamableHttp]` +- Runs the specified **transport manager** to handle client connections. +- Specify transport type on command line (default `stdio`) + - `stdio` → `transports/stdio.js` + - `sse` → `transports/sse.js` + - `streamableHttp` → `transports/streamableHttp.js` ## 2. The Transport Manager -- Creates a server instance using `createServer()` from `server/index.ts` - - Connects it to the chosen transport type from the MCP SDK. -- Handles communication according to the MCP specs for the chosen transport. - - **STDIO**: - - One simple, process‑bound connection. - - Calls`clientConnect()` upon connection. - - Closes and calls `cleanup()` on `SIGINT`. - - **SSE**: - - Supports multiple client connections. - - Client transports are mapped to `sessionId`; - - Calls `clientConnect(sessionId)` upon connection. - - Hooks server’s `onclose` to clean and remove session. - - Exposes - - `/sse` **GET** (SSE stream) - - `/message` **POST** (JSON‑RPC messages) - - **Streamable HTTP**: - - Supports multiple client connections. - - Client transports are mapped to `sessionId`; - - Calls `clientConnect(sessionId)` upon connection. - - Exposes `/mcp` for - - **POST** (JSON‑RPC messages) - - **GET** (SSE stream) - - **DELETE** (termination) - - Uses an event store for resumability and stores transports by `sessionId`. - - Calls `cleanup(sessionId)` on **DELETE**. +- Creates a server instance using `createServer()` from `server/index.ts` + - Connects it to the chosen transport type from the MCP SDK. +- Handles communication according to the MCP specs for the chosen transport. + - **STDIO**: + - One simple, process‑bound connection. + - Calls`clientConnect()` upon connection. + - Closes and calls `cleanup()` on `SIGINT`. + - **SSE**: + - Supports multiple client connections. + - Client transports are mapped to `sessionId`; + - Calls `clientConnect(sessionId)` upon connection. + - Hooks server’s `onclose` to clean and remove session. + - Exposes + - `/sse` **GET** (SSE stream) + - `/message` **POST** (JSON‑RPC messages) + - **Streamable HTTP**: + - Supports multiple client connections. + - Client transports are mapped to `sessionId`; + - Calls `clientConnect(sessionId)` upon connection. + - Exposes `/mcp` for + - **POST** (JSON‑RPC messages) + - **GET** (SSE stream) + - **DELETE** (termination) + - Uses an event store for resumability and stores transports by `sessionId`. + - Calls `cleanup(sessionId)` on **DELETE**. ## 3. The Server Factory -- Invoke `createServer()` from `server/index.ts` -- Creates a new `McpServer` instance with - - **Capabilities**: - - `tools: {}` - - `logging: {}` - - `prompts: {}` - - `resources: { subscribe: true }` - - **Server Instructions** - - Loaded from the docs folder (`server-instructions.md`). - - **Registrations** - - Registers **tools** via `registerTools(server)`. - - Registers **resources** via `registerResources(server)`. - - Registers **prompts** via `registerPrompts(server)`. - - **Other Request Handlers** - - Sets up resource subscription handlers via `setSubscriptionHandlers(server)`. - - Roots list change handler is added post-connection via - - **Returns** - - The `McpServer` instance - - A `clientConnect(sessionId)` callback that enables post-connection setup - - A `cleanup(sessionId?)` callback that stops any active intervals and removes any session‑scoped state +- Invoke `createServer()` from `server/index.ts` +- Creates a new `McpServer` instance with + - **Capabilities**: + - `tools: {}` + - `logging: {}` + - `prompts: {}` + - `resources: { subscribe: true }` + - **Server Instructions** + - Loaded from the docs folder (`server-instructions.md`). + - **Registrations** + - Registers **tools** via `registerTools(server)`. + - Registers **resources** via `registerResources(server)`. + - Registers **prompts** via `registerPrompts(server)`. + - **Other Request Handlers** + - Sets up resource subscription handlers via `setSubscriptionHandlers(server)`. + - Roots list change handler is added post-connection via + - **Returns** + - The `McpServer` instance + - A `clientConnect(sessionId)` callback that enables post-connection setup + - A `cleanup(sessionId?)` callback that stops any active intervals and removes any session‑scoped state ## Enabling Multiple Clients diff --git a/src/everything/docs/structure.md b/src/everything/docs/structure.md index bd3d70b95c..eeb343eb02 100644 --- a/src/everything/docs/structure.md +++ b/src/everything/docs/structure.md @@ -69,126 +69,126 @@ src/everything ### `index.ts` -- CLI entry point that selects and runs a specific transport module based on the first CLI argument: `stdio`, `sse`, or `streamableHttp`. +- CLI entry point that selects and runs a specific transport module based on the first CLI argument: `stdio`, `sse`, or `streamableHttp`. ### `AGENTS.md` -- Directions for Agents/LLMs explaining coding guidelines and how to appropriately extend the server. +- Directions for Agents/LLMs explaining coding guidelines and how to appropriately extend the server. ### `package.json` -- Package metadata and scripts: - - `build`: TypeScript compile to `dist/`, copies `docs/` into `dist/` and marks the compiled entry scripts as executable. - - `start:stdio`, `start:sse`, `start:streamableHttp`: Run built transports from `dist/`. -- Declares dependencies on `@modelcontextprotocol/sdk`, `express`, `cors`, `zod`, etc. +- Package metadata and scripts: + - `build`: TypeScript compile to `dist/`, copies `docs/` into `dist/` and marks the compiled entry scripts as executable. + - `start:stdio`, `start:sse`, `start:streamableHttp`: Run built transports from `dist/`. +- Declares dependencies on `@modelcontextprotocol/sdk`, `express`, `cors`, `zod`, etc. ### `docs/` -- `architecture.md` - - This document. -- `instructions.md` - - Human‑readable instructions intended to be passed to the client/LLM as guidance on server use. Loaded by the server at startup and returned in the initialize exchange. +- `architecture.md` + - This document. +- `instructions.md` + - Human‑readable instructions intended to be passed to the client/LLM as guidance on server use. Loaded by the server at startup and returned in the initialize exchange. ### `prompts/` -- `index.ts` - - `registerPrompts(server)` orchestrator; delegates to prompt factory/registration methods from in individual prompt files. -- `simple.ts` - - Registers `simple-prompt`: a prompt with no arguments that returns a single user 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 `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`. +- `index.ts` + - `registerPrompts(server)` orchestrator; delegates to prompt factory/registration methods from in individual prompt files. +- `simple.ts` + - Registers `simple-prompt`: a prompt with no arguments that returns a single user 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 `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 resource factory/registration methods from individual resource files. -- `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 positive integer. Content is generated on demand with a timestamp. - - 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`. +- `index.ts` + - `registerResources(server)` orchestrator; delegates to resource factory/registration methods from individual resource files. +- `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 positive integer. Content is generated on demand with a timestamp. + - 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`. ### `server/` -- `index.ts` - - Server factory that creates an `McpServer` with declared capabilities, loads server instructions, and registers tools, prompts, and resources. - - Sets resource subscription handlers via `setSubscriptionHandlers(server)`. - - Exposes `{ server, cleanup }` to the chosen transport. Cleanup stops any running intervals in the server when the transport disconnects. -- `logging.ts` - - Implements simulated logging. Periodically sends randomized log messages at various levels to the connected client session. Started/stopped on demand via a dedicated tool. +- `index.ts` + - Server factory that creates an `McpServer` with declared capabilities, loads server instructions, and registers tools, prompts, and resources. + - Sets resource subscription handlers via `setSubscriptionHandlers(server)`. + - Exposes `{ server, cleanup }` to the chosen transport. Cleanup stops any running intervals in the server when the transport disconnects. +- `logging.ts` + - Implements simulated logging. Periodically sends randomized log messages at various levels to the connected client session. Started/stopped on demand via a dedicated tool. ### `tools/` -- `index.ts` - - `registerTools(server)` orchestrator; delegates to tool factory/registration methods in individual tool files. -- `echo.ts` - - Registers an `echo` tool that takes a message and returns `Echo: {message}`. -- `get-annotated-message.ts` - - Registers a `get-annotated-message` tool which demonstrates content-level annotations. Emits a primary `text` message with content `annotations` (`priority`, `audience`) that vary by `messageType` (`"error" | "success" | "debug"`), and optionally includes an annotated `image` (tiny PNG) when `includeImage` is true. All tools in this server include tool-level annotations (`readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`). -- `get-env.ts` - - Registers a `get-env` tool that returns the current process environment variables as formatted JSON text; useful for debugging configuration. -- `get-resource-links.ts` - - Registers a `get-resource-links` tool that returns an intro `text` block followed by multiple `resource_link` items. -- `get-resource-reference.ts` - - Registers a `get-resource-reference` tool that returns a reference for a selected dynamic resource. -- `get-roots-list.ts` - - Registers a `get-roots-list` tool that returns the last list of roots sent by the client. -- `gzip-file-as-resource.ts` - - Registers a `gzip-file-as-resource` tool that fetches content from a URL or data URI, compresses it, and then either: - - returns a `resource_link` to a session-scoped resource (default), or - - returns an inline `resource` with the gzipped data. The resource will be still discoverable for the duration of the session via `resources/list`. - - Uses `resources/session.ts` to register the gzipped blob as a per-session resource at a URI like `demo://resource/session/` with `mimeType: application/gzip`. - - Environment controls: - - `GZIP_MAX_FETCH_SIZE` (bytes, default 10 MiB) - - `GZIP_MAX_FETCH_TIME_MILLIS` (ms, default 30000) - - `GZIP_ALLOWED_DOMAINS` (comma-separated allowlist; empty means all domains allowed) -- `simulate-research-query.ts` - - Registers a `simulate-research-query` task-based tool that demonstrates the MCP Tasks feature (SEP-1686). Simulates a multi-stage research operation with progress updates. If the query is marked as ambiguous and the client supports elicitation, it pauses mid-execution to request clarification via `elicitation/create`. Uses `server.experimental.tasks.registerToolTask()` with `execution: { taskSupport: "required" }`. -- `trigger-elicitation-request.ts` - - Registers a `trigger-elicitation-request` tool that sends an `elicitation/create` request to the client/LLM and returns the elicitation result. -- `trigger-url-elicitation.ts` - - Registers a `trigger-url-elicitation` tool that either sends an out-of-band URL-mode `elicitation/create` request (`mode: "url"`) including an `elicitationId` (request path) or throws `UrlElicitationRequiredError` (`-32042`) for client-handled URL elicitation (error path). On the error path the carried prerequisite elicitation points at a different URL than the failing one (`https://modelcontextprotocol.io`), and when the client satisfies it and retries the same call, the retry ignores `errorPath` and proceeds via the request path — so the client does not loop on the same error. -- `trigger-elicitation-request-async.ts` - - Registers a `trigger-elicitation-request-async` tool that demonstrates bidirectional MCP tasks for elicitation. Sends an elicitation request with task metadata, then polls the client's `tasks/get` endpoint for completion status before fetching the final result. -- `trigger-sampling-request.ts` - - Registers a `trigger-sampling-request` tool that sends a `sampling/createMessage` request to the client/LLM and returns the sampling result. -- `trigger-sampling-request-async.ts` - - Registers a `trigger-sampling-request-async` tool that demonstrates bidirectional MCP tasks for sampling. Sends a sampling request with task metadata, then polls the client's `tasks/get` endpoint for completion status before fetching the final result. -- `get-structured-content.ts` - - Registers a `get-structured-content` tool that demonstrates structuredContent block responses. -- `get-sum.ts` - - Registers a `get-sum` tool with a Zod input schema that sums two numbers `a` and `b` and returns the result. -- `get-tiny-image.ts` - - Registers a `get-tiny-image` tool, which returns a tiny PNG MCP logo as an `image` content item, along with surrounding descriptive `text` items. -- `trigger-long-running-operation.ts` - - Registers a `trigger-long-running-operation` tool that simulates a long-running task over a specified `duration` (seconds) and number of `steps`; emits `notifications/progress` updates when the client supplies a `progressToken`. -- `toggle-simulated-logging.ts` - - Registers a `toggle-simulated-logging` tool, which starts or stops simulated logging for the invoking session. -- `toggle-subscriber-updates.ts` - - Registers a `toggle-subscriber-updates` tool, which starts or stops simulated resource subscription update checks for the invoking session. +- `index.ts` + - `registerTools(server)` orchestrator; delegates to tool factory/registration methods in individual tool files. +- `echo.ts` + - Registers an `echo` tool that takes a message and returns `Echo: {message}`. +- `get-annotated-message.ts` + - Registers a `get-annotated-message` tool which demonstrates content-level annotations. Emits a primary `text` message with content `annotations` (`priority`, `audience`) that vary by `messageType` (`"error" | "success" | "debug"`), and optionally includes an annotated `image` (tiny PNG) when `includeImage` is true. All tools in this server include tool-level annotations (`readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`). +- `get-env.ts` + - Registers a `get-env` tool that returns the current process environment variables as formatted JSON text; useful for debugging configuration. +- `get-resource-links.ts` + - Registers a `get-resource-links` tool that returns an intro `text` block followed by multiple `resource_link` items. +- `get-resource-reference.ts` + - Registers a `get-resource-reference` tool that returns a reference for a selected dynamic resource. +- `get-roots-list.ts` + - Registers a `get-roots-list` tool that returns the last list of roots sent by the client. +- `gzip-file-as-resource.ts` + - Registers a `gzip-file-as-resource` tool that fetches content from a URL or data URI, compresses it, and then either: + - returns a `resource_link` to a session-scoped resource (default), or + - returns an inline `resource` with the gzipped data. The resource will be still discoverable for the duration of the session via `resources/list`. + - Uses `resources/session.ts` to register the gzipped blob as a per-session resource at a URI like `demo://resource/session/` with `mimeType: application/gzip`. + - Environment controls: + - `GZIP_MAX_FETCH_SIZE` (bytes, default 10 MiB) + - `GZIP_MAX_FETCH_TIME_MILLIS` (ms, default 30000) + - `GZIP_ALLOWED_DOMAINS` (comma-separated allowlist; empty means all domains allowed) +- `simulate-research-query.ts` + - Registers a `simulate-research-query` task-based tool that demonstrates the MCP Tasks feature (SEP-1686). Simulates a multi-stage research operation with progress updates. If the query is marked as ambiguous and the client supports elicitation, it pauses mid-execution to request clarification via `elicitation/create`. Uses `server.experimental.tasks.registerToolTask()` with `execution: { taskSupport: "required" }`. +- `trigger-elicitation-request.ts` + - Registers a `trigger-elicitation-request` tool that sends an `elicitation/create` request to the client/LLM and returns the elicitation result. +- `trigger-url-elicitation.ts` + - Registers a `trigger-url-elicitation` tool that either sends an out-of-band URL-mode `elicitation/create` request (`mode: "url"`) including an `elicitationId` (request path) or throws `UrlElicitationRequiredError` (`-32042`) for client-handled URL elicitation (error path). On the error path the carried prerequisite elicitation points at a different URL than the failing one (`https://modelcontextprotocol.io`), and when the client satisfies it and retries the same call, the retry ignores `errorPath` and proceeds via the request path — so the client does not loop on the same error. +- `trigger-elicitation-request-async.ts` + - Registers a `trigger-elicitation-request-async` tool that demonstrates bidirectional MCP tasks for elicitation. Sends an elicitation request with task metadata, then polls the client's `tasks/get` endpoint for completion status before fetching the final result. +- `trigger-sampling-request.ts` + - Registers a `trigger-sampling-request` tool that sends a `sampling/createMessage` request to the client/LLM and returns the sampling result. +- `trigger-sampling-request-async.ts` + - Registers a `trigger-sampling-request-async` tool that demonstrates bidirectional MCP tasks for sampling. Sends a sampling request with task metadata, then polls the client's `tasks/get` endpoint for completion status before fetching the final result. +- `get-structured-content.ts` + - Registers a `get-structured-content` tool that demonstrates structuredContent block responses. +- `get-sum.ts` + - Registers a `get-sum` tool with a Zod input schema that sums two numbers `a` and `b` and returns the result. +- `get-tiny-image.ts` + - Registers a `get-tiny-image` tool, which returns a tiny PNG MCP logo as an `image` content item, along with surrounding descriptive `text` items. +- `trigger-long-running-operation.ts` + - Registers a `trigger-long-running-operation` tool that simulates a long-running task over a specified `duration` (seconds) and number of `steps`; emits `notifications/progress` updates when the client supplies a `progressToken`. +- `toggle-simulated-logging.ts` + - Registers a `toggle-simulated-logging` tool, which starts or stops simulated logging for the invoking session. +- `toggle-subscriber-updates.ts` + - Registers a `toggle-subscriber-updates` tool, which starts or stops simulated resource subscription update checks for the invoking session. ### `transports/` -- `stdio.ts` - - Starts a `StdioServerTransport`, created the server via `createServer()`, and connects it. - - Handles `SIGINT` to close cleanly and calls `cleanup()` to remove any live intervals. -- `sse.ts` - - Express server exposing: - - `GET /sse` to establish an SSE connection per session. - - `POST /message` for client messages. - - Manages multiple connected clients via a transport map. - - Starts an `SSEServerTransport`, created the server via `createServer()`, and connects it to a new transport. - - On server disconnect, calls `cleanup()` to remove any live intervals. -- `streamableHttp.ts` - - Express server exposing a single `/mcp` endpoint for POST (JSON‑RPC), GET (SSE stream), and DELETE (session termination) using `StreamableHTTPServerTransport`. - - Uses an `InMemoryEventStore` for resumable sessions and tracks transports by `sessionId`. - - Connects a fresh server instance on initialization POST and reuses the transport for subsequent requests. +- `stdio.ts` + - Starts a `StdioServerTransport`, created the server via `createServer()`, and connects it. + - Handles `SIGINT` to close cleanly and calls `cleanup()` to remove any live intervals. +- `sse.ts` + - Express server exposing: + - `GET /sse` to establish an SSE connection per session. + - `POST /message` for client messages. + - Manages multiple connected clients via a transport map. + - Starts an `SSEServerTransport`, created the server via `createServer()`, and connects it to a new transport. + - On server disconnect, calls `cleanup()` to remove any live intervals. +- `streamableHttp.ts` + - Express server exposing a single `/mcp` endpoint for POST (JSON‑RPC), GET (SSE stream), and DELETE (session termination) using `StreamableHTTPServerTransport`. + - Uses an `InMemoryEventStore` for resumable sessions and tracks transports by `sessionId`. + - Connects a fresh server instance on initialization POST and reuses the transport for subsequent requests. diff --git a/src/everything/index.ts b/src/everything/index.ts index 39d50fa651..f591211535 100644 --- a/src/everything/index.ts +++ b/src/everything/index.ts @@ -2,41 +2,41 @@ // Parse command line arguments first const args = process.argv.slice(2); -const scriptName = args[0] || "stdio"; +const scriptName = args[0] || 'stdio'; async function run() { - try { - // Dynamically import only the requested module to prevent all modules from initializing - switch (scriptName) { - case "stdio": - // Import and run the default server - await import("./transports/stdio.js"); - break; - case "sse": - // Import and run the SSE server - await import("./transports/sse.js"); - break; - case "streamableHttp": - // Import and run the streamable HTTP server - await import("./transports/streamableHttp.js"); - break; - default: - console.error(`-`.repeat(53)); - console.error(` Everything Server Launcher`); - console.error(` Usage: node ./index.js [stdio|sse|streamableHttp]`); - console.error(` Default transport: stdio`); - console.error(`-`.repeat(53)); - console.error(`Unknown transport: ${scriptName}`); - console.log("Available transports:"); - console.log("- stdio"); - console.log("- sse"); - console.log("- streamableHttp"); + try { + // Dynamically import only the requested module to prevent all modules from initializing + switch (scriptName) { + case 'stdio': + // Import and run the default server + await import('./transports/stdio.js'); + break; + case 'sse': + // Import and run the SSE server + await import('./transports/sse.js'); + break; + case 'streamableHttp': + // Import and run the streamable HTTP server + await import('./transports/streamableHttp.js'); + break; + default: + console.error(`-`.repeat(53)); + console.error(` Everything Server Launcher`); + console.error(` Usage: node ./index.js [stdio|sse|streamableHttp]`); + console.error(` Default transport: stdio`); + console.error(`-`.repeat(53)); + console.error(`Unknown transport: ${scriptName}`); + console.log('Available transports:'); + console.log('- stdio'); + console.log('- sse'); + console.log('- streamableHttp'); + process.exit(1); + } + } catch (error) { + console.error('Error running script:', error); process.exit(1); } - } catch (error) { - console.error("Error running script:", error); - process.exit(1); - } } await run(); diff --git a/src/everything/package.json b/src/everything/package.json index 4f8b836407..614d865b06 100644 --- a/src/everything/package.json +++ b/src/everything/package.json @@ -1,48 +1,51 @@ { - "name": "@modelcontextprotocol/server-everything", - "version": "2.0.0", - "description": "MCP server that exercises all the features of the MCP protocol", - "license": "SEE LICENSE IN LICENSE", - "mcpName": "io.github.modelcontextprotocol/server-everything", - "author": "Model Context Protocol a Series of LF Projects, LLC.", - "homepage": "https://modelcontextprotocol.io", - "bugs": "https://github.com/modelcontextprotocol/servers/issues", - "repository": { - "type": "git", - "url": "https://github.com/modelcontextprotocol/servers.git" - }, - "type": "module", - "bin": { - "mcp-server-everything": "dist/index.js" - }, - "files": [ - "dist" - ], - "scripts": { - "build": "tsc && shx cp -r docs dist/ && shx chmod +x dist/*.js", - "prepare": "npm run build", - "watch": "tsc --watch", - "start:stdio": "node dist/index.js stdio", - "start:sse": "node dist/index.js sse", - "start:streamableHttp": "node dist/index.js streamableHttp", - "prettier:fix": "prettier --write .", - "prettier:check": "prettier --check .", - "test": "vitest run --coverage" - }, - "dependencies": { - "@modelcontextprotocol/sdk": "^1.29.0", - "cors": "^2.8.5", - "express": "^5.2.1", - "jszip": "^3.10.1", - "zod": "^4.0.0" - }, - "devDependencies": { - "@types/cors": "^2.8.19", - "@types/express": "^5.0.6", - "@vitest/coverage-v8": "^4.1.8", - "prettier": "^2.8.8", - "shx": "^0.3.4", - "typescript": "^5.6.2", - "vitest": "^4.1.8" - } + "name": "@modelcontextprotocol/server-everything", + "version": "2.0.0", + "description": "MCP server that exercises all the features of the MCP protocol", + "license": "SEE LICENSE IN LICENSE", + "mcpName": "io.github.modelcontextprotocol/server-everything", + "author": "Model Context Protocol a Series of LF Projects, LLC.", + "homepage": "https://modelcontextprotocol.io", + "bugs": "https://github.com/modelcontextprotocol/servers/issues", + "repository": { + "type": "git", + "url": "https://github.com/modelcontextprotocol/servers.git" + }, + "type": "module", + "bin": { + "mcp-server-everything": "dist/index.js" + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsc && shx cp -r docs dist/ && shx chmod +x dist/*.js", + "prepare": "npm run build", + "watch": "tsc --watch", + "start:stdio": "node dist/index.js stdio", + "start:sse": "node dist/index.js sse", + "start:streamableHttp": "node dist/index.js streamableHttp", + "prettier:fix": "prettier --write .", + "prettier:check": "prettier --check .", + "test": "vitest run --coverage" + }, + "dependencies": { + "cors": "^2.8.5", + "express": "^5.2.1", + "jszip": "^3.10.1", + "zod": "^4.0.0", + "@modelcontextprotocol/server": "^2.0.0-alpha.3", + "@modelcontextprotocol/core": "^2.0.0-alpha.1", + "@modelcontextprotocol/server-legacy": "^2.0.0-alpha.3", + "@modelcontextprotocol/node": "^2.0.0-alpha.3" + }, + "devDependencies": { + "@types/cors": "^2.8.19", + "@types/express": "^5.0.6", + "@vitest/coverage-v8": "^4.1.8", + "prettier": "^2.8.8", + "shx": "^0.3.4", + "typescript": "^5.6.2", + "vitest": "^4.1.8" + } } diff --git a/src/everything/prompts/args.ts b/src/everything/prompts/args.ts index 7e445a4ce4..65f1d9bba1 100644 --- a/src/everything/prompts/args.ts +++ b/src/everything/prompts/args.ts @@ -1,5 +1,5 @@ -import { z } from "zod"; -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from 'zod'; +import { McpServer } from '@modelcontextprotocol/server'; /** * Register a prompt with arguments @@ -9,33 +9,34 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; * @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(), - }; + // 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( - "args-prompt", - { - title: "Arguments Prompt", - description: "A prompt with two arguments, one required and one optional", - argsSchema: promptArgsSchema, - }, - (args) => { - const location = `${args?.city}${args?.state ? `, ${args?.state}` : ""}`; - return { - messages: [ - { - role: "user", - content: { - type: "text", - text: `What's weather in ${location}?`, - }, - }, - ], - }; - } - ); + // Register the prompt + /* @mcp-codemod-error Could not verify `argsSchema` is a schema object. Raw shapes are deprecated in v2 — pass a Standard Schema object (e.g. z.object({ … })); no change is needed if it already is one. */ + server.registerPrompt( + 'args-prompt', + { + title: 'Arguments Prompt', + description: 'A prompt with two arguments, one required and one optional', + argsSchema: promptArgsSchema + }, + args => { + const location = `${args?.city}${args?.state ? `, ${args?.state}` : ''}`; + return { + messages: [ + { + role: 'user', + content: { + type: 'text', + text: `What's weather in ${location}?` + } + } + ] + }; + } + ); }; diff --git a/src/everything/prompts/completions.ts b/src/everything/prompts/completions.ts index e47c36e57b..8090f73ec3 100644 --- a/src/everything/prompts/completions.ts +++ b/src/everything/prompts/completions.ts @@ -1,6 +1,5 @@ -import { z } from "zod"; -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { completable } from "@modelcontextprotocol/sdk/server/completable.js"; +import { z } from 'zod'; +import { McpServer, completable } from '@modelcontextprotocol/server'; /** * Register a prompt with completable arguments @@ -11,54 +10,45 @@ import { completable } from "@modelcontextprotocol/sdk/server/completable.js"; * @param server */ export const registerPromptWithCompletions = (server: McpServer) => { - // Prompt arguments - const promptArgsSchema = { - department: completable( - z.string().describe("Choose the department."), - (value) => { - return ["Engineering", "Sales", "Marketing", "Support"].filter((d) => - d.startsWith(value) - ); - } - ), - 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 []; - } - ), - }; + // Prompt arguments + const promptArgsSchema = { + department: completable(z.string().describe('Choose the department.'), value => { + return ['Engineering', 'Sales', 'Marketing', 'Support'].filter(d => d.startsWith(value)); + }), + 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: "First argument choice narrows values for second argument.", - argsSchema: promptArgsSchema, - }, - ({ department, name }) => ({ - messages: [ + // Register the prompt + /* @mcp-codemod-error Could not verify `argsSchema` is a schema object. Raw shapes are deprecated in v2 — pass a Standard Schema object (e.g. z.object({ … })); no change is needed if it already is one. */ + server.registerPrompt( + 'completable-prompt', { - role: "user", - content: { - type: "text", - text: `Please promote ${name} to the head of the ${department} team.`, - }, + title: 'Team Management', + description: 'First argument choice narrows values for second argument.', + argsSchema: promptArgsSchema }, - ], - }) - ); + ({ department, name }) => ({ + messages: [ + { + role: 'user', + content: { + type: 'text', + text: `Please promote ${name} to the head of the ${department} team.` + } + } + ] + }) + ); }; diff --git a/src/everything/prompts/index.ts b/src/everything/prompts/index.ts index 6efa7b7297..e70c890add 100644 --- a/src/everything/prompts/index.ts +++ b/src/everything/prompts/index.ts @@ -1,8 +1,8 @@ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { registerSimplePrompt } from "./simple.js"; -import { registerArgumentsPrompt } from "./args.js"; -import { registerPromptWithCompletions } from "./completions.js"; -import { registerEmbeddedResourcePrompt } from "./resource.js"; +import { McpServer } from '@modelcontextprotocol/server'; +import { registerSimplePrompt } from './simple.js'; +import { registerArgumentsPrompt } from './args.js'; +import { registerPromptWithCompletions } from './completions.js'; +import { registerEmbeddedResourcePrompt } from './resource.js'; /** * Register the prompts with the MCP server. @@ -10,8 +10,8 @@ import { registerEmbeddedResourcePrompt } from "./resource.js"; * @param server */ export const registerPrompts = (server: McpServer) => { - registerSimplePrompt(server); - registerArgumentsPrompt(server); - registerPromptWithCompletions(server); - registerEmbeddedResourcePrompt(server); + registerSimplePrompt(server); + registerArgumentsPrompt(server); + registerPromptWithCompletions(server); + registerEmbeddedResourcePrompt(server); }; diff --git a/src/everything/prompts/resource.ts b/src/everything/prompts/resource.ts index 03989aaa25..9c4cc94f12 100644 --- a/src/everything/prompts/resource.ts +++ b/src/everything/prompts/resource.ts @@ -1,17 +1,14 @@ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { McpServer } from '@modelcontextprotocol/server'; +import { resourceTypeCompleter, resourceIdForPromptCompleter } from '../resources/templates.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"; + textResource, + textResourceUri, + blobResourceUri, + blobResource, + RESOURCE_TYPE_BLOB, + RESOURCE_TYPE_TEXT, + RESOURCE_TYPES +} from '../resources/templates.js'; /** * Register a prompt with an embedded resource reference @@ -21,73 +18,56 @@ import { * @param server */ export const registerEmbeddedResourcePrompt = (server: McpServer) => { - // Prompt arguments - const promptArgsSchema = { - resourceType: resourceTypeCompleter, - resourceId: resourceIdForPromptCompleter, - }; + // Prompt arguments + const promptArgsSchema = { + resourceType: resourceTypeCompleter, + resourceId: resourceIdForPromptCompleter + }; - // Register the prompt - server.registerPrompt( - "resource-prompt", - { - title: "Resource Prompt", - description: "A prompt that includes an embedded resource reference", - argsSchema: promptArgsSchema, - }, - (args) => { - // Validate resource type argument - 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 ${RESOURCE_TYPE_TEXT} or ${RESOURCE_TYPE_BLOB}.` - ); - } + // Register the prompt + /* @mcp-codemod-error Could not verify `argsSchema` is a schema object. Raw shapes are deprecated in v2 — pass a Standard Schema object (e.g. z.object({ … })); no change is needed if it already is one. */ + server.registerPrompt( + 'resource-prompt', + { + title: 'Resource Prompt', + description: 'A prompt that includes an embedded resource reference', + argsSchema: promptArgsSchema + }, + args => { + // Validate resource type argument + 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 ${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.` - ); - } + // 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); + // 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 { - messages: [ - { - role: "user", - content: { - type: "text", - text: `This prompt includes the ${resourceType} resource with id: ${resourceId}. Please analyze the following resource:`, - }, - }, - { - role: "user", - content: { - type: "resource", - resource: resource, - }, - }, - ], - }; - } - ); + return { + messages: [ + { + role: 'user', + content: { + type: 'text', + text: `This prompt includes the ${resourceType} resource with id: ${resourceId}. Please analyze the following resource:` + } + }, + { + role: 'user', + content: { + type: 'resource', + resource: resource + } + } + ] + }; + } + ); }; diff --git a/src/everything/prompts/simple.ts b/src/everything/prompts/simple.ts index a2a0d2eea6..4e185aa038 100644 --- a/src/everything/prompts/simple.ts +++ b/src/everything/prompts/simple.ts @@ -1,4 +1,4 @@ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { McpServer } from '@modelcontextprotocol/server'; /** * Register a simple prompt with no arguments @@ -7,23 +7,23 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; * @param server */ export const registerSimplePrompt = (server: McpServer) => { - // Register the prompt - server.registerPrompt( - "simple-prompt", - { - title: "Simple Prompt", - description: "A prompt with no arguments", - }, - () => ({ - messages: [ + // Register the prompt + server.registerPrompt( + 'simple-prompt', { - role: "user", - content: { - type: "text", - text: "This is a simple prompt without arguments.", - }, + title: 'Simple Prompt', + description: 'A prompt with no arguments' }, - ], - }) - ); + () => ({ + messages: [ + { + role: 'user', + content: { + type: 'text', + text: 'This is a simple prompt without arguments.' + } + } + ] + }) + ); }; diff --git a/src/everything/resources/files.ts b/src/everything/resources/files.ts index e38cb59633..3887e2024a 100644 --- a/src/everything/resources/files.ts +++ b/src/everything/resources/files.ts @@ -1,7 +1,7 @@ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { dirname, join } from "path"; -import { fileURLToPath } from "url"; -import { readdirSync, readFileSync, statSync } from "fs"; +import { McpServer } from '@modelcontextprotocol/server'; +import { dirname, join } from 'path'; +import { fileURLToPath } from 'url'; +import { readdirSync, readFileSync, statSync } from 'fs'; /** * Register static file resources @@ -14,53 +14,48 @@ import { readdirSync, readFileSync, statSync } from "fs"; * @param server */ 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 - return; - } - - // Register each file as a static resource - for (const name of entries) { - // Only process files, not directories - const fullPath = join(docsDir, name); + // 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 { - const st = statSync(fullPath); - if (!st.isFile()) continue; - } catch { - continue; + entries = readdirSync(docsDir); + } catch (e) { + // If docs/ folder is missing or unreadable, just skip registration + return; } - // Prepare file resource info - const uri = `demo://resource/static/document/${encodeURIComponent(name)}`; - const mimeType = getMimeType(name); - const description = `Static document file exposed from /docs: ${name}`; + // 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); + if (!st.isFile()) continue; + } catch { + continue; + } + + // Prepare file resource info + const uri = `demo://resource/static/document/${encodeURIComponent(name)}`; + const mimeType = getMimeType(name); + const description = `Static document file exposed from /docs: ${name}`; - // Register file resource - server.registerResource( - name, - uri, - { mimeType, description }, - async (uri) => { - const text = readFileSafe(fullPath); - return { - contents: [ - { - uri: uri.toString(), - mimeType, - text, - }, - ], - }; - } - ); - } + // Register file resource + server.registerResource(name, uri, { mimeType, description }, async uri => { + const text = readFileSafe(fullPath); + return { + contents: [ + { + uri: uri.toString(), + mimeType, + text + } + ] + }; + }); + } }; /** @@ -68,12 +63,11 @@ export const registerFileResources = (server: McpServer) => { * @param fileName */ function getMimeType(fileName: string): string { - const lower = fileName.toLowerCase(); - if (lower.endsWith(".md") || lower.endsWith(".markdown")) - return "text/markdown"; - if (lower.endsWith(".txt")) return "text/plain"; - if (lower.endsWith(".json")) return "application/json"; - return "text/plain"; + const lower = fileName.toLowerCase(); + if (lower.endsWith('.md') || lower.endsWith('.markdown')) return 'text/markdown'; + if (lower.endsWith('.txt')) return 'text/plain'; + if (lower.endsWith('.json')) return 'application/json'; + return 'text/plain'; } /** @@ -81,9 +75,9 @@ function getMimeType(fileName: string): string { * @param path */ function readFileSafe(path: string): string { - try { - return readFileSync(path, "utf-8"); - } catch (e) { - return `Error reading file: ${path}. ${e}`; - } + try { + return readFileSync(path, 'utf-8'); + } catch (e) { + return `Error reading file: ${path}. ${e}`; + } } diff --git a/src/everything/resources/index.ts b/src/everything/resources/index.ts index 30c6f7dcf8..96d27106bd 100644 --- a/src/everything/resources/index.ts +++ b/src/everything/resources/index.ts @@ -1,17 +1,17 @@ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { registerResourceTemplates } from "./templates.js"; -import { registerFileResources } from "./files.js"; -import { fileURLToPath } from "url"; -import { dirname, join } from "path"; -import { readFileSync } from "fs"; +import { McpServer } from '@modelcontextprotocol/server'; +import { registerResourceTemplates } from './templates.js'; +import { registerFileResources } from './files.js'; +import { fileURLToPath } from 'url'; +import { dirname, join } from 'path'; +import { readFileSync } from 'fs'; /** * Register the resources with the MCP server. * @param server */ export const registerResources = (server: McpServer) => { - registerResourceTemplates(server); - registerFileResources(server); + registerResourceTemplates(server); + registerFileResources(server); }; /** @@ -22,15 +22,15 @@ export const registerResources = (server: McpServer) => { * @return {string} The content of the server instructions file, or an error message if reading fails. */ export function readInstructions(): string { - const __filename = fileURLToPath(import.meta.url); - const __dirname = dirname(__filename); - const filePath = join(__dirname, "..", "docs", "instructions.md"); - let instructions; + const __filename = fileURLToPath(import.meta.url); + const __dirname = dirname(__filename); + const filePath = join(__dirname, '..', 'docs', 'instructions.md'); + let instructions; - try { - instructions = readFileSync(filePath, "utf-8"); - } catch (e) { - instructions = "Server instructions not loaded: " + e; - } - return instructions; + try { + instructions = readFileSync(filePath, 'utf-8'); + } catch (e) { + instructions = 'Server instructions not loaded: ' + e; + } + return instructions; } diff --git a/src/everything/resources/session.ts b/src/everything/resources/session.ts index 10e0db33c1..6944e7205d 100644 --- a/src/everything/resources/session.ts +++ b/src/everything/resources/session.ts @@ -1,5 +1,4 @@ -import { McpServer, RegisteredResource } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { Resource, ResourceLink } from "@modelcontextprotocol/sdk/types.js"; +import { McpServer, RegisteredResource, Resource, ResourceLink } from '@modelcontextprotocol/server'; /** * Tracks registered session resources by URI to allow updating/removing on re-registration. @@ -15,7 +14,7 @@ const registeredResources = new Map(); * @returns {string} The formatted session resource URI. */ export const getSessionResourceURI = (name: string): string => { - return `demo://resource/session/${name}`; + return `demo://resource/session/${name}`; }; /** @@ -29,52 +28,41 @@ export const getSessionResourceURI = (name: string): string => { * @param payload * @returns {ResourceLink} An object representing the resource link, with associated metadata. */ -export const registerSessionResource = ( - server: McpServer, - resource: Resource, - type: "text" | "blob", - payload: string -): ResourceLink => { - // Destructure resource - const { uri, name, mimeType, description, title, annotations, icons, _meta } = - resource; +export const registerSessionResource = (server: McpServer, resource: Resource, type: 'text' | 'blob', payload: string): ResourceLink => { + // Destructure resource + const { uri, name, mimeType, description, title, annotations, icons, _meta } = resource; - // Prepare the resource content to return - // See https://modelcontextprotocol.io/specification/2025-11-25/server/resources#resource-contents - const resourceContent = - type === "text" - ? { - uri: uri.toString(), - mimeType, - text: payload, - } - : { - uri: uri.toString(), - mimeType, - blob: payload, - }; - - // Check if a resource with this URI is already registered and remove it - const existingResource = registeredResources.get(uri); - if (existingResource) { - existingResource.remove(); - registeredResources.delete(uri); - } + // Prepare the resource content to return + // See https://modelcontextprotocol.io/specification/2025-11-25/server/resources#resource-contents + const resourceContent = + type === 'text' + ? { + uri: uri.toString(), + mimeType, + text: payload + } + : { + uri: uri.toString(), + mimeType, + blob: payload + }; - // Register file resource - const registeredResource = server.registerResource( - name, - uri, - { mimeType, description, title, annotations, icons, _meta }, - async () => { - return { - contents: [resourceContent], - }; + // Check if a resource with this URI is already registered and remove it + const existingResource = registeredResources.get(uri); + if (existingResource) { + existingResource.remove(); + registeredResources.delete(uri); } - ); - // Track the registered resource for potential future removal - registeredResources.set(uri, registeredResource); + // Register file resource + const registeredResource = server.registerResource(name, uri, { mimeType, description, title, annotations, icons, _meta }, async () => { + return { + contents: [resourceContent] + }; + }); + + // Track the registered resource for potential future removal + registeredResources.set(uri, registeredResource); - return { type: "resource_link", ...resource }; + return { type: 'resource_link', ...resource }; }; diff --git a/src/everything/resources/subscriptions.ts b/src/everything/resources/subscriptions.ts index 2a5e57460f..23e081f58a 100644 --- a/src/everything/resources/subscriptions.ts +++ b/src/everything/resources/subscriptions.ts @@ -1,18 +1,10 @@ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { - SubscribeRequestSchema, - UnsubscribeRequestSchema, -} from "@modelcontextprotocol/sdk/types.js"; +import { McpServer } from '@modelcontextprotocol/server'; // Track subscriber session id lists by URI -const subscriptions: Map> = new Map< - string, - Set ->(); +const subscriptions: Map> = new Map>(); // Interval to send notifications to subscribers -const subsUpdateIntervals: Map = - new Map(); +const subsUpdateIntervals: Map = new Map(); /** * Sets up the subscription and unsubscription handlers for the provided server. @@ -34,66 +26,54 @@ const subsUpdateIntervals: Map = * @param {McpServer} server - The server instance to which subscription handlers will be attached. */ export const setSubscriptionHandlers = (server: McpServer) => { - // Set the subscription handler - server.server.setRequestHandler( - SubscribeRequestSchema, - async (request, extra) => { - // Get the URI to subscribe to - const { uri } = request.params; + // Set the subscription handler + server.server.setRequestHandler('resources/subscribe', async (request, ctx) => { + // Get the URI to subscribe to + const { uri } = request.params; - // Get the session id (can be undefined for stdio) - const sessionId = extra.sessionId as string; + // Get the session id (can be undefined for stdio) + const sessionId = ctx.sessionId as string; - // Acknowledge the subscribe request - await server.sendLoggingMessage( - { - level: "info", - data: `Received Subscribe Resource request for URI: ${uri} ${ - sessionId ? `from session ${sessionId}` : "" - }`, - }, - sessionId - ); + // Acknowledge the subscribe request + await server.sendLoggingMessage( + { + level: 'info', + data: `Received Subscribe Resource request for URI: ${uri} ${sessionId ? `from session ${sessionId}` : ''}` + }, + sessionId + ); - // Get the subscribers for this URI - const subscribers = subscriptions.has(uri) - ? (subscriptions.get(uri) as Set) - : new Set(); - subscribers.add(sessionId); - subscriptions.set(uri, subscribers); - return {}; - } - ); + // Get the subscribers for this URI + const subscribers = subscriptions.has(uri) ? (subscriptions.get(uri) as Set) : new Set(); + subscribers.add(sessionId); + subscriptions.set(uri, subscribers); + return {}; + }); - // Set the unsubscription handler - server.server.setRequestHandler( - UnsubscribeRequestSchema, - async (request, extra) => { - // Get the URI to subscribe to - const { uri } = request.params; + // Set the unsubscription handler + server.server.setRequestHandler('resources/unsubscribe', async (request, ctx) => { + // Get the URI to subscribe to + const { uri } = request.params; - // Get the session id (can be undefined for stdio) - const sessionId = extra.sessionId as string; + // Get the session id (can be undefined for stdio) + const sessionId = ctx.sessionId as string; - // Acknowledge the subscribe request - await server.sendLoggingMessage( - { - level: "info", - data: `Received Unsubscribe Resource request: ${uri} ${ - sessionId ? `from session ${sessionId}` : "" - }`, - }, - sessionId - ); + // Acknowledge the subscribe request + await server.sendLoggingMessage( + { + level: 'info', + data: `Received Unsubscribe Resource request: ${uri} ${sessionId ? `from session ${sessionId}` : ''}` + }, + sessionId + ); - // Remove the subscriber - if (subscriptions.has(uri)) { - const subscribers = subscriptions.get(uri) as Set; - if (subscribers.has(sessionId)) subscribers.delete(sessionId); - } - return {}; - } - ); + // Remove the subscriber + if (subscriptions.has(uri)) { + const subscribers = subscriptions.get(uri) as Set; + if (subscribers.has(sessionId)) subscribers.delete(sessionId); + } + return {}; + }); }; /** @@ -108,24 +88,21 @@ export const setSubscriptionHandlers = (server: McpServer) => { * @param {string | undefined} sessionId - The session ID of the client to check for subscriptions. * @returns {Promise} Resolves once all applicable notifications are sent. */ -const sendSimulatedResourceUpdates = async ( - server: McpServer, - sessionId: string | undefined -): Promise => { - // Search all URIs for ones this client is subscribed to - for (const uri of subscriptions.keys()) { - const subscribers = subscriptions.get(uri) as Set; +const sendSimulatedResourceUpdates = async (server: McpServer, sessionId: string | undefined): Promise => { + // Search all URIs for ones this client is subscribed to + for (const uri of subscriptions.keys()) { + const subscribers = subscriptions.get(uri) as Set; - // If this client is subscribed, send the notification - if (subscribers.has(sessionId)) { - await server.server.notification({ - method: "notifications/resources/updated", - params: { uri }, - }); - } else { - subscribers.delete(sessionId); // subscriber has disconnected + // If this client is subscribed, send the notification + if (subscribers.has(sessionId)) { + await server.server.notification({ + method: 'notifications/resources/updated', + params: { uri } + }); + } else { + subscribers.delete(sessionId); // subscriber has disconnected + } } - } }; /** @@ -136,20 +113,17 @@ const sendSimulatedResourceUpdates = async ( * @param server * @param sessionId */ -export const beginSimulatedResourceUpdates = ( - server: McpServer, - sessionId: string | undefined -) => { - if (!subsUpdateIntervals.has(sessionId)) { - // Send once immediately - sendSimulatedResourceUpdates(server, sessionId); +export const beginSimulatedResourceUpdates = (server: McpServer, sessionId: string | undefined) => { + if (!subsUpdateIntervals.has(sessionId)) { + // Send once immediately + sendSimulatedResourceUpdates(server, sessionId); - // Set the interval to send later resource update notifications to this client - subsUpdateIntervals.set( - sessionId, - setInterval(() => sendSimulatedResourceUpdates(server, sessionId), 5000) - ); - } + // Set the interval to send later resource update notifications to this client + subsUpdateIntervals.set( + sessionId, + setInterval(() => sendSimulatedResourceUpdates(server, sessionId), 5000) + ); + } }; /** @@ -162,10 +136,10 @@ export const beginSimulatedResourceUpdates = ( * @param {string} [sessionId] */ export const stopSimulatedResourceUpdates = (sessionId?: string) => { - // Remove active intervals - if (subsUpdateIntervals.has(sessionId)) { - const subsUpdateInterval = subsUpdateIntervals.get(sessionId); - clearInterval(subsUpdateInterval); - subsUpdateIntervals.delete(sessionId); - } + // Remove active intervals + if (subsUpdateIntervals.has(sessionId)) { + const subsUpdateInterval = subsUpdateIntervals.get(sessionId); + clearInterval(subsUpdateInterval); + subsUpdateIntervals.delete(sessionId); + } }; diff --git a/src/everything/resources/templates.ts b/src/everything/resources/templates.ts index 6d4903f74c..9a51f70ba1 100644 --- a/src/everything/resources/templates.ts +++ b/src/everything/resources/templates.ts @@ -1,18 +1,10 @@ -import { z } from "zod"; -import { - CompleteResourceTemplateCallback, - McpServer, - ResourceTemplate, -} from "@modelcontextprotocol/sdk/server/mcp.js"; -import { completable } from "@modelcontextprotocol/sdk/server/completable.js"; +import { z } from 'zod'; +import { CompleteResourceTemplateCallback, McpServer, ResourceTemplate, completable } from '@modelcontextprotocol/server'; // 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, -]; +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. @@ -24,12 +16,9 @@ export const RESOURCE_TYPES: string[] = [ * 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)); - } -); +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. @@ -47,13 +36,10 @@ export const resourceTypeCompleter = completable( * 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) => { +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 @@ -64,14 +50,13 @@ export const resourceIdForPromptCompleter = completable( * @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) => { +export const resourceIdForResourceTemplateCompleter: CompleteResourceTemplateCallback = (value: string) => { const resourceId = Number(value); return Number.isInteger(resourceId) && resourceId > 0 ? [value] : []; - }; +}; -const uriBase: string = "demo://resource/dynamic"; +const uriBase: string = 'demo://resource/dynamic'; const textUriBase: string = `${uriBase}/text`; const blobUriBase: string = `${uriBase}/blob`; const textUriTemplate: string = `${textUriBase}/{resourceId}`; @@ -84,12 +69,12 @@ const blobUriTemplate: string = `${blobUriBase}/{resourceId}`; * @param resourceId */ export const textResource = (uri: URL, resourceId: number) => { - const timestamp = new Date().toLocaleTimeString(); - return { - uri: uri.toString(), - mimeType: "text/plain", - text: `Resource ${resourceId}: This is a plaintext resource created at ${timestamp}`, - }; + const timestamp = new Date().toLocaleTimeString(); + return { + uri: uri.toString(), + mimeType: 'text/plain', + text: `Resource ${resourceId}: This is a plaintext resource created at ${timestamp}` + }; }; /** @@ -99,15 +84,13 @@ export const textResource = (uri: URL, resourceId: number) => { * @param resourceId */ export const blobResource = (uri: URL, resourceId: number) => { - const timestamp = new Date().toLocaleTimeString(); - const resourceText = Buffer.from( - `Resource ${resourceId}: This is a base64 blob created at ${timestamp}` - ).toString("base64"); - return { - uri: uri.toString(), - mimeType: "text/plain", - blob: resourceText, - }; + const timestamp = new Date().toLocaleTimeString(); + const resourceText = Buffer.from(`Resource ${resourceId}: This is a base64 blob created at ${timestamp}`).toString('base64'); + return { + uri: uri.toString(), + mimeType: 'text/plain', + blob: resourceText + }; }; /** @@ -115,16 +98,14 @@ export const blobResource = (uri: URL, resourceId: number) => { * - Exposed for use by embedded resource prompt example * @param resourceId */ -export const textResourceUri = (resourceId: number) => - new URL(`${textUriBase}/${resourceId}`); +export const textResourceUri = (resourceId: number) => new URL(`${textUriBase}/${resourceId}`); /** * Create a dynamic blob resource URI * - Exposed for use by embedded resource prompt example * @param resourceId */ -export const blobResourceUri = (resourceId: number) => - new URL(`${blobUriBase}/${resourceId}`); +export const blobResourceUri = (resourceId: number) => new URL(`${blobUriBase}/${resourceId}`); /** * Parses the resource identifier from the provided URI and validates it @@ -137,21 +118,18 @@ export const blobResourceUri = (resourceId: number) => * @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; + const uriError = `Unknown resource: ${uri.toString()}`; + if (uri.toString().startsWith(textUriBase) && uri.toString().startsWith(blobUriBase)) { + throw new Error(uriError); } else { - throw new Error(uriError); + 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); + } } - } }; /** @@ -169,43 +147,41 @@ const parseResourceId = (uri: URL, variables: Record) => { * @param server */ export const registerResourceTemplates = (server: McpServer) => { - // Register the text resource template - server.registerResource( - "Dynamic Text Resource", - new ResourceTemplate(textUriTemplate, { - list: undefined, - complete: { resourceId: resourceIdForResourceTemplateCompleter }, - }), - { - mimeType: "text/plain", - description: - "Plaintext dynamic resource fabricated from the {resourceId} variable, which must be an integer.", - }, - async (uri, variables) => { - const resourceId = parseResourceId(uri, variables); - return { - contents: [textResource(uri, resourceId)], - }; - } - ); + // Register the text resource template + server.registerResource( + 'Dynamic Text Resource', + new ResourceTemplate(textUriTemplate, { + list: undefined, + complete: { resourceId: resourceIdForResourceTemplateCompleter } + }), + { + mimeType: 'text/plain', + description: 'Plaintext dynamic resource fabricated from the {resourceId} variable, which must be an integer.' + }, + async (uri, variables) => { + const resourceId = parseResourceId(uri, variables); + return { + contents: [textResource(uri, resourceId)] + }; + } + ); - // Register the blob resource template - server.registerResource( - "Dynamic Blob Resource", - new ResourceTemplate(blobUriTemplate, { - list: undefined, - complete: { resourceId: resourceIdForResourceTemplateCompleter }, - }), - { - mimeType: "application/octet-stream", - description: - "Binary (base64) dynamic resource fabricated from the {resourceId} variable, which must be an integer.", - }, - async (uri, variables) => { - const resourceId = parseResourceId(uri, variables); - return { - contents: [blobResource(uri, resourceId)], - }; - } - ); + // Register the blob resource template + server.registerResource( + 'Dynamic Blob Resource', + new ResourceTemplate(blobUriTemplate, { + list: undefined, + complete: { resourceId: resourceIdForResourceTemplateCompleter } + }), + { + mimeType: 'application/octet-stream', + description: 'Binary (base64) dynamic resource fabricated from the {resourceId} variable, which must be an integer.' + }, + async (uri, variables) => { + const resourceId = parseResourceId(uri, variables); + return { + contents: [blobResource(uri, resourceId)] + }; + } + ); }; diff --git a/src/everything/server/index.ts b/src/everything/server/index.ts index f1459cc812..6948182777 100644 --- a/src/everything/server/index.ts +++ b/src/everything/server/index.ts @@ -1,22 +1,15 @@ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { - InMemoryTaskStore, - InMemoryTaskMessageQueue, -} from "@modelcontextprotocol/sdk/experimental/tasks"; -import { - setSubscriptionHandlers, - stopSimulatedResourceUpdates, -} from "../resources/subscriptions.js"; -import { registerConditionalTools, registerTools } from "../tools/index.js"; -import { registerResources, readInstructions } from "../resources/index.js"; -import { registerPrompts } from "../prompts/index.js"; -import { stopSimulatedLogging } from "./logging.js"; -import { syncRoots } from "./roots.js"; +import { McpServer } from '@modelcontextprotocol/server'; +import { setSubscriptionHandlers, stopSimulatedResourceUpdates } from '../resources/subscriptions.js'; +import { registerConditionalTools, registerTools } from '../tools/index.js'; +import { registerResources, readInstructions } from '../resources/index.js'; +import { registerPrompts } from '../prompts/index.js'; +import { stopSimulatedLogging } from './logging.js'; +import { syncRoots } from './roots.js'; // Server Factory response export type ServerFactoryResponse = { - server: McpServer; - cleanup: (sessionId?: string) => void; + server: McpServer; + cleanup: (sessionId?: string) => void; }; /** @@ -33,86 +26,78 @@ export type ServerFactoryResponse = { * - `cleanup` {Function}: Function to perform cleanup operations for a closing session. */ export const createServer: () => ServerFactoryResponse = () => { - // Read the server instructions - const instructions = readInstructions(); + // Read the server instructions + const instructions = readInstructions(); - // Create task store and message queue for task support - const taskStore = new InMemoryTaskStore(); - const taskMessageQueue = new InMemoryTaskMessageQueue(); + let initializeTimeout: NodeJS.Timeout | null = null; - let initializeTimeout: NodeJS.Timeout | null = null; - - // Create the server - const server = new McpServer( - { - name: "mcp-servers/everything", - title: "Everything Reference Server", - version: "2.0.0", - }, - { - capabilities: { - tools: { - listChanged: true, - }, - prompts: { - listChanged: true, + // Create the server + const server = new McpServer( + { + name: 'mcp-servers/everything', + title: 'Everything Reference Server', + version: '2.0.0' }, - resources: { - subscribe: true, - listChanged: true, - }, - logging: {}, - tasks: { - list: {}, - cancel: {}, - requests: { - tools: { - call: {}, + { + capabilities: { + tools: { + listChanged: true + }, + prompts: { + listChanged: true + }, + resources: { + subscribe: true, + listChanged: true + }, + logging: {}, + tasks: { + list: {}, + cancel: {}, + requests: { + tools: { + call: {} + } + } + } }, - }, - }, - }, - instructions, - taskStore, - taskMessageQueue, - } - ); + instructions + } + ); - // Register the tools - registerTools(server); + // Register the tools + registerTools(server); - // Register the resources - registerResources(server); + // Register the resources + registerResources(server); - // Register the prompts - registerPrompts(server); + // Register the prompts + registerPrompts(server); - // Set resource subscription handlers - setSubscriptionHandlers(server); + // Set resource subscription handlers + setSubscriptionHandlers(server); - // Perform post-initialization operations - server.server.oninitialized = async () => { - // Register conditional tools now that client capabilities are known. - // This finishes before the `notifications/initialized` handler finishes. - registerConditionalTools(server); + // Perform post-initialization operations + server.server.oninitialized = async () => { + // Register conditional tools now that client capabilities are known. + // This finishes before the `notifications/initialized` handler finishes. + registerConditionalTools(server); - // Sync roots if the client supports them. - // This is delayed until after the `notifications/initialized` handler finishes, - // otherwise, the request gets lost. - const sessionId = server.server.transport?.sessionId; - initializeTimeout = setTimeout(() => syncRoots(server, sessionId), 350); - }; + // Sync roots if the client supports them. + // This is delayed until after the `notifications/initialized` handler finishes, + // otherwise, the request gets lost. + const sessionId = server.server.transport?.sessionId; + initializeTimeout = setTimeout(() => syncRoots(server, sessionId), 350); + }; - // Return the ServerFactoryResponse - return { - server, - cleanup: (sessionId?: string) => { - // Stop any simulated logging or resource updates that may have been initiated. - stopSimulatedLogging(sessionId); - stopSimulatedResourceUpdates(sessionId); - // Clean up task store timers - taskStore.cleanup(); - if (initializeTimeout) clearTimeout(initializeTimeout); - }, - } satisfies ServerFactoryResponse; + // Return the ServerFactoryResponse + return { + server, + cleanup: (sessionId?: string) => { + // Stop any simulated logging or resource updates that may have been initiated. + stopSimulatedLogging(sessionId); + stopSimulatedResourceUpdates(sessionId); + if (initializeTimeout) clearTimeout(initializeTimeout); + } + } satisfies ServerFactoryResponse; }; diff --git a/src/everything/server/logging.ts b/src/everything/server/logging.ts index 82edea162c..6f54ae058e 100644 --- a/src/everything/server/logging.ts +++ b/src/everything/server/logging.ts @@ -1,9 +1,7 @@ -import { LoggingLevel } from "@modelcontextprotocol/sdk/types.js"; -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { LoggingLevel, McpServer } from '@modelcontextprotocol/server'; // Map session ID to the interval for sending logging messages to the client -const logsUpdateIntervals: Map = - new Map(); +const logsUpdateIntervals: Map = new Map(); /** * Initiates a simulated logging process by sending random log messages to the client at a @@ -13,54 +11,48 @@ const logsUpdateIntervals: Map = * @param {string | undefined} sessionId - An optional identifier for the session. If provided, * the session ID will be appended to log messages. */ -export const beginSimulatedLogging = ( - server: McpServer, - sessionId: string | undefined -) => { - const maybeAppendSessionId = sessionId ? ` - SessionId ${sessionId}` : ""; - const messages: { level: LoggingLevel; data: string }[] = [ - { level: "debug", data: `Debug-level message${maybeAppendSessionId}` }, - { level: "info", data: `Info-level message${maybeAppendSessionId}` }, - { level: "notice", data: `Notice-level message${maybeAppendSessionId}` }, - { - level: "warning", - data: `Warning-level message${maybeAppendSessionId}`, - }, - { level: "error", data: `Error-level message${maybeAppendSessionId}` }, - { - level: "critical", - data: `Critical-level message${maybeAppendSessionId}`, - }, - { level: "alert", data: `Alert level-message${maybeAppendSessionId}` }, - { - level: "emergency", - data: `Emergency-level message${maybeAppendSessionId}`, - }, - ]; +export const beginSimulatedLogging = (server: McpServer, sessionId: string | undefined) => { + const maybeAppendSessionId = sessionId ? ` - SessionId ${sessionId}` : ''; + const messages: { level: LoggingLevel; data: string }[] = [ + { level: 'debug', data: `Debug-level message${maybeAppendSessionId}` }, + { level: 'info', data: `Info-level message${maybeAppendSessionId}` }, + { level: 'notice', data: `Notice-level message${maybeAppendSessionId}` }, + { + level: 'warning', + data: `Warning-level message${maybeAppendSessionId}` + }, + { level: 'error', data: `Error-level message${maybeAppendSessionId}` }, + { + level: 'critical', + data: `Critical-level message${maybeAppendSessionId}` + }, + { level: 'alert', data: `Alert level-message${maybeAppendSessionId}` }, + { + level: 'emergency', + data: `Emergency-level message${maybeAppendSessionId}` + } + ]; - /** - * Send a simulated logging message to the client - */ - const sendSimulatedLoggingMessage = async (sessionId: string | undefined) => { - // By using the `sendLoggingMessage` function to send the message, we - // ensure that the client's chosen logging level will be respected - await server.sendLoggingMessage( - messages[Math.floor(Math.random() * messages.length)], - sessionId - ); - }; + /** + * Send a simulated logging message to the client + */ + const sendSimulatedLoggingMessage = async (sessionId: string | undefined) => { + // By using the `sendLoggingMessage` function to send the message, we + // ensure that the client's chosen logging level will be respected + await server.sendLoggingMessage(messages[Math.floor(Math.random() * messages.length)], sessionId); + }; - // Set the interval to send later logging messages to this client - if (!logsUpdateIntervals.has(sessionId)) { - // Send once immediately - sendSimulatedLoggingMessage(sessionId); + // Set the interval to send later logging messages to this client + if (!logsUpdateIntervals.has(sessionId)) { + // Send once immediately + sendSimulatedLoggingMessage(sessionId); - // Send a randomly-leveled log message every 5 seconds - logsUpdateIntervals.set( - sessionId, - setInterval(() => sendSimulatedLoggingMessage(sessionId), 5000) - ); - } + // Send a randomly-leveled log message every 5 seconds + logsUpdateIntervals.set( + sessionId, + setInterval(() => sendSimulatedLoggingMessage(sessionId), 5000) + ); + } }; /** @@ -73,10 +65,10 @@ export const beginSimulatedLogging = ( * @param {string} [sessionId] - The optional unique identifier of the session. */ export const stopSimulatedLogging = (sessionId?: string) => { - // Remove active intervals - if (logsUpdateIntervals.has(sessionId)) { - const logsUpdateInterval = logsUpdateIntervals.get(sessionId); - clearInterval(logsUpdateInterval); - logsUpdateIntervals.delete(sessionId); - } + // Remove active intervals + if (logsUpdateIntervals.has(sessionId)) { + const logsUpdateInterval = logsUpdateIntervals.get(sessionId); + clearInterval(logsUpdateInterval); + logsUpdateIntervals.delete(sessionId); + } }; diff --git a/src/everything/server/roots.ts b/src/everything/server/roots.ts index 34b12b21ce..00d6d3701f 100644 --- a/src/everything/server/roots.ts +++ b/src/everything/server/roots.ts @@ -1,14 +1,7 @@ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { - Root, - RootsListChangedNotificationSchema, -} from "@modelcontextprotocol/sdk/types.js"; +import { McpServer, Root } from '@modelcontextprotocol/server'; // Track roots by session id -export const roots: Map = new Map< - string | undefined, - Root[] ->(); +export const roots: Map = new Map(); /** * Get the latest the client roots list for the session. @@ -29,62 +22,57 @@ export const roots: Map = new Map< * @throws {Error} In case of a failure to request the roots from the client, an error log message is sent. */ export const syncRoots = async (server: McpServer, sessionId?: string) => { - const clientCapabilities = server.server.getClientCapabilities() || {}; - const clientSupportsRoots: boolean = clientCapabilities?.roots !== undefined; + const clientCapabilities = server.server.getClientCapabilities() || {}; + const clientSupportsRoots: boolean = clientCapabilities?.roots !== undefined; - // Fetch the roots list for this client - if (clientSupportsRoots) { - // Function to request the updated roots list from the client - const requestRoots = async () => { - try { - // Request the updated roots list from the client - const response = await server.server.listRoots(); - if (response && "roots" in response) { - // Store the roots list for this client - roots.set(sessionId, response.roots); + // Fetch the roots list for this client + if (clientSupportsRoots) { + // Function to request the updated roots list from the client + const requestRoots = async () => { + try { + // Request the updated roots list from the client + const response = await server.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 - ); - } else { - await server.sendLoggingMessage( - { - level: "info", - logger: "everything-server", - data: "Client returned no roots set", - }, - sessionId - ); - } - } catch (error) { - console.error( - `Failed to request roots from client ${sessionId}: ${ - error instanceof Error ? error.message : String(error) - }` - ); - } - }; + // 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 + ); + } else { + await server.sendLoggingMessage( + { + level: 'info', + logger: 'everything-server', + data: 'Client returned no roots set' + }, + sessionId + ); + } + } catch (error) { + console.error( + `Failed to request roots from client ${sessionId}: ${error instanceof Error ? error.message : String(error)}` + ); + } + }; - // If the roots have not been synced for this client, - // set notification handler and request initial roots - if (!roots.has(sessionId)) { - // Set the list changed notification handler - server.server.setNotificationHandler( - RootsListChangedNotificationSchema, - requestRoots - ); + // If the roots have not been synced for this client, + // set notification handler and request initial roots + if (!roots.has(sessionId)) { + // Set the list changed notification handler + server.server.setNotificationHandler('notifications/roots/list_changed', requestRoots); - // Request the initial roots list immediately - await requestRoots(); - } + // Request the initial roots list immediately + await requestRoots(); + } - // Return the roots list for this client - return roots.get(sessionId); - } + // Return the roots list for this client + return roots.get(sessionId); + } }; diff --git a/src/everything/tools/echo.ts b/src/everything/tools/echo.ts index 0658e83195..db5a4db836 100644 --- a/src/everything/tools/echo.ts +++ b/src/everything/tools/echo.ts @@ -1,24 +1,23 @@ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; -import { z } from "zod"; +import { McpServer, CallToolResult } from '@modelcontextprotocol/server'; +import { z } from 'zod'; // Tool input schema export const EchoSchema = z.object({ - message: z.string().describe("Message to echo"), + message: z.string().describe('Message to echo') }); // Tool configuration -const name = "echo"; +const name = 'echo'; const config = { - title: "Echo Tool", - description: "Echoes back the input string", - inputSchema: EchoSchema, - annotations: { - readOnlyHint: true, - destructiveHint: false, - idempotentHint: true, - openWorldHint: false, - }, + title: 'Echo Tool', + description: 'Echoes back the input string', + inputSchema: EchoSchema, + annotations: { + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: false + } }; /** @@ -31,10 +30,10 @@ const config = { * @returns {void} */ export const registerEchoTool = (server: McpServer) => { - server.registerTool(name, config, async (args): Promise => { - const validatedArgs = EchoSchema.parse(args); - return { - content: [{ type: "text", text: `Echo: ${validatedArgs.message}` }], - }; - }); + server.registerTool(name, config, async (args): Promise => { + const validatedArgs = EchoSchema.parse(args); + return { + content: [{ type: 'text', text: `Echo: ${validatedArgs.message}` }] + }; + }); }; diff --git a/src/everything/tools/get-annotated-message.ts b/src/everything/tools/get-annotated-message.ts index 5de72b029a..743f8c97ad 100644 --- a/src/everything/tools/get-annotated-message.ts +++ b/src/everything/tools/get-annotated-message.ts @@ -1,32 +1,25 @@ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; -import { z } from "zod"; -import { MCP_TINY_IMAGE } from "./get-tiny-image.js"; +import { McpServer, CallToolResult } from '@modelcontextprotocol/server'; +import { z } from 'zod'; +import { MCP_TINY_IMAGE } from './get-tiny-image.js'; // Tool input schema const GetAnnotatedMessageSchema = z.object({ - messageType: z - .enum(["error", "success", "debug"]) - .describe("Type of message to demonstrate different annotation patterns"), - includeImage: z - .boolean() - .default(false) - .describe("Whether to include an example image"), + messageType: z.enum(['error', 'success', 'debug']).describe('Type of message to demonstrate different annotation patterns'), + includeImage: z.boolean().default(false).describe('Whether to include an example image') }); // Tool configuration -const name = "get-annotated-message"; +const name = 'get-annotated-message'; const config = { - title: "Get Annotated Message Tool", - description: - "Demonstrates how annotations can be used to provide metadata about content.", - inputSchema: GetAnnotatedMessageSchema, - annotations: { - readOnlyHint: true, // This tool only returns data, no side effects - destructiveHint: false, // Does not delete or modify anything - idempotentHint: true, // Same input always produces same output - openWorldHint: false, // Does not interact with external systems - }, + title: 'Get Annotated Message Tool', + description: 'Demonstrates how annotations can be used to provide metadata about content.', + inputSchema: GetAnnotatedMessageSchema, + annotations: { + readOnlyHint: true, // This tool only returns data, no side effects + destructiveHint: false, // Does not delete or modify anything + idempotentHint: true, // Same input always produces same output + openWorldHint: false // Does not interact with external systems + } }; /** @@ -42,54 +35,54 @@ const config = { * @param {McpServer} server - The McpServer instance where the tool will be registered. */ export const registerGetAnnotatedMessageTool = (server: McpServer) => { - server.registerTool(name, config, async (args): Promise => { - const { messageType, includeImage } = GetAnnotatedMessageSchema.parse(args); + server.registerTool(name, config, async (args): Promise => { + const { messageType, includeImage } = GetAnnotatedMessageSchema.parse(args); - const content: CallToolResult["content"] = []; + const content: CallToolResult['content'] = []; - // Main message with different priorities/audiences based on type - if (messageType === "error") { - content.push({ - type: "text", - text: "Error: Operation failed", - annotations: { - priority: 1.0, // Errors are highest priority - audience: ["user", "assistant"], // Both need to know about errors - }, - }); - } else if (messageType === "success") { - content.push({ - type: "text", - text: "Operation completed successfully", - annotations: { - priority: 0.7, // Success messages are important but not critical - audience: ["user"], // Success mainly for user consumption - }, - }); - } else if (messageType === "debug") { - content.push({ - type: "text", - text: "Debug: Cache hit ratio 0.95, latency 150ms", - annotations: { - priority: 0.3, // Debug info is low priority - audience: ["assistant"], // Technical details for assistant - }, - }); - } + // Main message with different priorities/audiences based on type + if (messageType === 'error') { + content.push({ + type: 'text', + text: 'Error: Operation failed', + annotations: { + priority: 1.0, // Errors are highest priority + audience: ['user', 'assistant'] // Both need to know about errors + } + }); + } else if (messageType === 'success') { + content.push({ + type: 'text', + text: 'Operation completed successfully', + annotations: { + priority: 0.7, // Success messages are important but not critical + audience: ['user'] // Success mainly for user consumption + } + }); + } else if (messageType === 'debug') { + content.push({ + type: 'text', + text: 'Debug: Cache hit ratio 0.95, latency 150ms', + annotations: { + priority: 0.3, // Debug info is low priority + audience: ['assistant'] // Technical details for assistant + } + }); + } - // Optional image with its own annotations - if (includeImage) { - content.push({ - type: "image", - data: MCP_TINY_IMAGE, - mimeType: "image/png", - annotations: { - priority: 0.5, - audience: ["user"], // Images primarily for user visualization - }, - }); - } + // Optional image with its own annotations + if (includeImage) { + content.push({ + type: 'image', + data: MCP_TINY_IMAGE, + mimeType: 'image/png', + annotations: { + priority: 0.5, + audience: ['user'] // Images primarily for user visualization + } + }); + } - return { content }; - }); + return { content }; + }); }; diff --git a/src/everything/tools/get-env.ts b/src/everything/tools/get-env.ts index 55eabfaa97..7e0a16109b 100644 --- a/src/everything/tools/get-env.ts +++ b/src/everything/tools/get-env.ts @@ -1,19 +1,17 @@ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import { McpServer, CallToolResult } from '@modelcontextprotocol/server'; // Tool configuration -const name = "get-env"; +const name = 'get-env'; const config = { - title: "Print Environment Tool", - description: - "Returns all environment variables, helpful for debugging MCP server configuration", - inputSchema: {}, - annotations: { - readOnlyHint: true, - destructiveHint: false, - idempotentHint: true, - openWorldHint: false, - }, + title: 'Print Environment Tool', + description: 'Returns all environment variables, helpful for debugging MCP server configuration', + inputSchema: {}, + annotations: { + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: false + } }; /** @@ -26,14 +24,14 @@ const config = { * @returns {void} */ export const registerGetEnvTool = (server: McpServer) => { - server.registerTool(name, config, async (args): Promise => { - return { - content: [ - { - type: "text", - text: JSON.stringify(process.env, null, 2), - }, - ], - }; - }); + server.registerTool(name, config, async (args): Promise => { + return { + content: [ + { + type: 'text', + text: JSON.stringify(process.env, null, 2) + } + ] + }; + }); }; diff --git a/src/everything/tools/get-resource-links.ts b/src/everything/tools/get-resource-links.ts index 7684cb64a5..06ebf8f4cc 100644 --- a/src/everything/tools/get-resource-links.ts +++ b/src/everything/tools/get-resource-links.ts @@ -1,36 +1,24 @@ -import { z } from "zod"; -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; -import { - textResource, - textResourceUri, - blobResourceUri, - blobResource, -} from "../resources/templates.js"; +import { z } from 'zod'; +import { McpServer, CallToolResult } from '@modelcontextprotocol/server'; +import { textResource, textResourceUri, blobResourceUri, blobResource } from '../resources/templates.js'; // Tool input schema const GetResourceLinksSchema = z.object({ - count: z - .number() - .min(1) - .max(10) - .default(3) - .describe("Number of resource links to return (1-10)"), + count: z.number().min(1).max(10).default(3).describe('Number of resource links to return (1-10)') }); // Tool configuration -const name = "get-resource-links"; +const name = 'get-resource-links'; const config = { - title: "Get Resource Links Tool", - description: - "Returns up to ten resource links that reference different types of resources", - inputSchema: GetResourceLinksSchema, - annotations: { - readOnlyHint: true, - destructiveHint: false, - idempotentHint: true, - openWorldHint: false, - }, + title: 'Get Resource Links Tool', + description: 'Returns up to ten resource links that reference different types of resources', + inputSchema: GetResourceLinksSchema, + annotations: { + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: false + } }; /** @@ -45,42 +33,36 @@ const config = { * @param {McpServer} server - The McpServer instance where the tool will be registered. */ export const registerGetResourceLinksTool = (server: McpServer) => { - server.registerTool(name, config, async (args): Promise => { - const { count } = GetResourceLinksSchema.parse(args); + server.registerTool(name, config, async (args): Promise => { + const { count } = GetResourceLinksSchema.parse(args); - // Add intro text content block - const content: CallToolResult["content"] = []; - content.push({ - type: "text", - text: `Here are ${count} resource links to resources available in this server:`, - }); + // Add intro text content block + const content: CallToolResult['content'] = []; + content.push({ + type: 'text', + text: `Here are ${count} resource links to resources available in this server:` + }); - // Create resource link content blocks - for (let resourceId = 1; resourceId <= count; resourceId++) { - // Get resource uri for text or blob resource based on odd/even resourceId - const isOdd = resourceId % 2 === 0; - const uri = isOdd - ? textResourceUri(resourceId) - : blobResourceUri(resourceId); + // Create resource link content blocks + for (let resourceId = 1; resourceId <= count; resourceId++) { + // Get resource uri for text or blob resource based on odd/even resourceId + const isOdd = resourceId % 2 === 0; + const uri = isOdd ? textResourceUri(resourceId) : blobResourceUri(resourceId); - // Get resource based on the resource type - const resource = isOdd - ? textResource(uri, resourceId) - : blobResource(uri, resourceId); + // Get resource based on the resource type + const resource = isOdd ? textResource(uri, resourceId) : blobResource(uri, resourceId); - content.push({ - type: "resource_link", - uri: resource.uri, - name: `${isOdd ? "Text" : "Blob"} Resource ${resourceId}`, - description: `Resource ${resourceId}: ${ - resource.mimeType === "text/plain" - ? "plaintext resource" - : "binary blob resource" - }`, - mimeType: resource.mimeType, - }); - } + content.push({ + type: 'resource_link', + uri: resource.uri, + name: `${isOdd ? 'Text' : 'Blob'} Resource ${resourceId}`, + description: `Resource ${resourceId}: ${ + resource.mimeType === 'text/plain' ? 'plaintext resource' : 'binary blob resource' + }`, + mimeType: resource.mimeType + }); + } - return { content }; - }); + return { content }; + }); }; diff --git a/src/everything/tools/get-resource-reference.ts b/src/everything/tools/get-resource-reference.ts index 5806365868..a872eb91a8 100644 --- a/src/everything/tools/get-resource-reference.ts +++ b/src/everything/tools/get-resource-reference.ts @@ -1,39 +1,33 @@ -import { z } from "zod"; -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import { z } from 'zod'; +import { McpServer, CallToolResult } from '@modelcontextprotocol/server'; import { - textResource, - textResourceUri, - blobResourceUri, - blobResource, - RESOURCE_TYPE_BLOB, - RESOURCE_TYPE_TEXT, - RESOURCE_TYPES, -} from "../resources/templates.js"; + 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"), + 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 name = 'get-resource-reference'; const config = { - title: "Get Resource Reference Tool", - description: "Returns a resource reference that can be used by MCP clients", - inputSchema: GetResourceReferenceSchema, - annotations: { - readOnlyHint: true, - destructiveHint: false, - idempotentHint: true, - openWorldHint: false, - }, + title: 'Get Resource Reference Tool', + description: 'Returns a resource reference that can be used by MCP clients', + inputSchema: GetResourceReferenceSchema, + annotations: { + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: false + } }; /** @@ -53,52 +47,38 @@ const config = { * @param {McpServer} server - The McpServer instance where the tool will be 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}.` - ); - } + 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.` - ); - } + // 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); + // 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}`, - }, - ], - }; - }); + 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/get-roots-list.ts b/src/everything/tools/get-roots-list.ts index a8778e4c01..b3daaf5c44 100644 --- a/src/everything/tools/get-roots-list.ts +++ b/src/everything/tools/get-roots-list.ts @@ -1,20 +1,19 @@ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; -import { syncRoots } from "../server/roots.js"; +import { McpServer, CallToolResult } from '@modelcontextprotocol/server'; +import { syncRoots } from '../server/roots.js'; // Tool configuration -const name = "get-roots-list"; +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: {}, - annotations: { - readOnlyHint: true, - destructiveHint: false, - idempotentHint: true, - openWorldHint: false, - }, + 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: {}, + annotations: { + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: false + } }; /** @@ -35,64 +34,53 @@ const config = { * @param {McpServer} server - The McpServer instance where the tool will be registered. */ export const registerGetRootsListTool = (server: McpServer) => { - // Does client support roots? - const clientCapabilities = server.server.getClientCapabilities() || {}; - const clientSupportsRoots: boolean = clientCapabilities.roots !== undefined; + // Does client support roots? + const clientCapabilities = server.server.getClientCapabilities() || {}; + const clientSupportsRoots: boolean = clientCapabilities.roots !== undefined; - // If so, register tool - if (clientSupportsRoots) { - server.registerTool( - name, - config, - async (args, extra): Promise => { - // Get the current rootsFetch the current roots list from the client if need be - const currentRoots = await syncRoots(server, extra.sessionId); + // If so, register tool + if (clientSupportsRoots) { + server.registerTool(name, config, async (args, ctx): Promise => { + // Get the current rootsFetch the current roots list from the client if need be + const currentRoots = await syncRoots(server, ctx.sessionId); - // Respond if client supports roots but doesn't have any configured - if ( - clientSupportsRoots && - (!currentRoots || 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", - }, - ], - }; - } + // Respond if client supports roots but doesn't have any configured + if (clientSupportsRoots && (!currentRoots || 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' + } + ] + }; + } - // Create formatted response if there is a list of roots - const rootsList = currentRoots - ? currentRoots - .map((root, index) => { - return `${index + 1}. ${root.name || "Unnamed Root"}\n URI: ${ - root.uri - }`; - }) - .join("\n\n") - : "No roots found"; + // Create formatted response if there is a list of roots + const rootsList = currentRoots + ? currentRoots + .map((root, index) => { + return `${index + 1}. ${root.name || 'Unnamed Root'}\n URI: ${root.uri}`; + }) + .join('\n\n') + : 'No roots found'; - 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.", - }, - ], - }; - } - ); - } + 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.' + } + ] + }; + }); + } }; diff --git a/src/everything/tools/get-structured-content.ts b/src/everything/tools/get-structured-content.ts index f9bde5c9e4..b9bcc4cca5 100644 --- a/src/everything/tools/get-structured-content.ts +++ b/src/everything/tools/get-structured-content.ts @@ -1,38 +1,31 @@ -import { z } from "zod"; -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { - CallToolResult, - ContentBlock, -} from "@modelcontextprotocol/sdk/types.js"; +import { z } from 'zod'; +import { McpServer, CallToolResult, ContentBlock } from '@modelcontextprotocol/server'; // Tool input schema const GetStructuredContentInputSchema = { - location: z - .enum(["New York", "Chicago", "Los Angeles"]) - .describe("Choose city"), + location: z.enum(['New York', 'Chicago', 'Los Angeles']).describe('Choose city') }; // Tool output schema const GetStructuredContentOutputSchema = z.object({ - temperature: z.number().describe("Temperature in celsius"), - conditions: z.string().describe("Weather conditions description"), - humidity: z.number().describe("Humidity percentage"), + temperature: z.number().describe('Temperature in celsius'), + conditions: z.string().describe('Weather conditions description'), + humidity: z.number().describe('Humidity percentage') }); // Tool configuration -const name = "get-structured-content"; +const name = 'get-structured-content'; const config = { - title: "Get Structured Content Tool", - description: - "Returns structured content along with an output schema for client data validation", - inputSchema: GetStructuredContentInputSchema, - outputSchema: GetStructuredContentOutputSchema, - annotations: { - readOnlyHint: true, - destructiveHint: false, - idempotentHint: true, - openWorldHint: false, - }, + title: 'Get Structured Content Tool', + description: 'Returns structured content along with an output schema for client data validation', + inputSchema: GetStructuredContentInputSchema, + outputSchema: GetStructuredContentOutputSchema, + annotations: { + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: false + } }; /** @@ -50,43 +43,43 @@ const config = { * @param {McpServer} server - The McpServer instance where the tool will be registered. */ export const registerGetStructuredContentTool = (server: McpServer) => { - server.registerTool(name, config, async (args): Promise => { - // Get simulated weather for the chosen city - let weather; - switch (args.location) { - case "New York": - weather = { - temperature: 33, - conditions: "Cloudy", - humidity: 82, - }; - break; + server.registerTool(name, config, async (args): Promise => { + // Get simulated weather for the chosen city + let weather; + switch (args.location) { + case 'New York': + weather = { + temperature: 33, + conditions: 'Cloudy', + humidity: 82 + }; + break; - case "Chicago": - weather = { - temperature: 36, - conditions: "Light rain / drizzle", - humidity: 82, - }; - break; + case 'Chicago': + weather = { + temperature: 36, + conditions: 'Light rain / drizzle', + humidity: 82 + }; + break; - case "Los Angeles": - weather = { - temperature: 73, - conditions: "Sunny / Clear", - humidity: 48, - }; - break; - } + case 'Los Angeles': + weather = { + temperature: 73, + conditions: 'Sunny / Clear', + humidity: 48 + }; + break; + } - const backwardCompatibleContentBlock: ContentBlock = { - type: "text", - text: JSON.stringify(weather), - }; + const backwardCompatibleContentBlock: ContentBlock = { + type: 'text', + text: JSON.stringify(weather) + }; - return { - content: [backwardCompatibleContentBlock], - structuredContent: weather, - }; - }); + return { + content: [backwardCompatibleContentBlock], + structuredContent: weather + }; + }); }; diff --git a/src/everything/tools/get-sum.ts b/src/everything/tools/get-sum.ts index 470293bf6f..dcee73e483 100644 --- a/src/everything/tools/get-sum.ts +++ b/src/everything/tools/get-sum.ts @@ -1,25 +1,24 @@ -import { z } from "zod"; -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import { z } from 'zod'; +import { McpServer, CallToolResult } from '@modelcontextprotocol/server'; // Tool input schema const GetSumSchema = z.object({ - a: z.number().describe("First number"), - b: z.number().describe("Second number"), + a: z.number().describe('First number'), + b: z.number().describe('Second number') }); // Tool configuration -const name = "get-sum"; +const name = 'get-sum'; const config = { - title: "Get Sum Tool", - description: "Returns the sum of two numbers", - inputSchema: GetSumSchema, - annotations: { - readOnlyHint: true, - destructiveHint: false, - idempotentHint: true, - openWorldHint: false, - }, + title: 'Get Sum Tool', + description: 'Returns the sum of two numbers', + inputSchema: GetSumSchema, + annotations: { + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: false + } }; /** @@ -36,16 +35,16 @@ const config = { * @param {McpServer} server - The McpServer instance where the tool will be registered. */ export const registerGetSumTool = (server: McpServer) => { - server.registerTool(name, config, async (args): Promise => { - const validatedArgs = GetSumSchema.parse(args); - const sum = validatedArgs.a + validatedArgs.b; - return { - content: [ - { - type: "text", - text: `The sum of ${validatedArgs.a} and ${validatedArgs.b} is ${sum}.`, - }, - ], - }; - }); + server.registerTool(name, config, async (args): Promise => { + const validatedArgs = GetSumSchema.parse(args); + const sum = validatedArgs.a + validatedArgs.b; + return { + content: [ + { + type: 'text', + text: `The sum of ${validatedArgs.a} and ${validatedArgs.b} is ${sum}.` + } + ] + }; + }); }; diff --git a/src/everything/tools/get-tiny-image.ts b/src/everything/tools/get-tiny-image.ts index c38c3c698b..bf34107e87 100644 --- a/src/everything/tools/get-tiny-image.ts +++ b/src/everything/tools/get-tiny-image.ts @@ -1,22 +1,21 @@ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import { McpServer, CallToolResult } from '@modelcontextprotocol/server'; // A tiny encoded MCP logo image export const MCP_TINY_IMAGE = - "iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAKsGlDQ1BJQ0MgUHJvZmlsZQAASImVlwdUU+kSgOfe9JDQEiIgJfQmSCeAlBBaAAXpYCMkAUKJMRBU7MriClZURLCs6KqIgo0idizYFsWC3QVZBNR1sWDDlXeBQ9jdd9575805c+a7c+efmf+e/z9nLgCdKZDJMlF1gCxpjjwyyI8dn5DIJvUABRiY0kBdIMyWcSMiwgCTUft3+dgGyJC9YzuU69/f/1fREImzhQBIBMbJomxhFsbHMe0TyuQ5ALg9mN9kbo5siK9gzJRjDWL8ZIhTR7hviJOHGY8fjomO5GGsDUCmCQTyVACaKeZn5wpTsTw0f4ztpSKJFGPsGbyzsmaLMMbqgiUWI8N4KD8n+S95Uv+WM1mZUyBIVfLIXoaF7C/JlmUK5v+fn+N/S1amYrSGOaa0NHlwJGaxvpAHGbNDlSxNnhI+yhLRcPwwpymCY0ZZmM1LHGWRwD9UuTZzStgop0gC+co8OfzoURZnB0SNsnx2pLJWipzHHWWBfKyuIiNG6U8T85X589Ki40Y5VxI7ZZSzM6JCx2J4Sr9cEansXywN8hurG6jce1b2X/Yr4SvX5qRFByv3LhjrXyzljuXMjlf2JhL7B4zFxCjjZTl+ylqyzAhlvDgzSOnPzo1Srs3BDuTY2gjlN0wXhESMMoRBELAhBjIhB+QggECQgBTEOeJ5Q2cUeLNl8+WS1LQcNhe7ZWI2Xyq0m8B2tHd0Bhi6syNH4j1r+C4irGtjvhWVAF4nBgcHT475Qm4BHEkCoNaO+SxnAKh3A1w5JVTIc0d8Q9cJCEAFNWCCDhiACViCLTiCK3iCLwRACIRDNCTATBBCGmRhnc+FhbAMCqAI1sNmKIOdsBv2wyE4CvVwCs7DZbgOt+AePIZ26IJX0AcfYQBBEBJCRxiIDmKImCE2iCPCQbyRACQMiUQSkCQkFZEiCmQhsgIpQoqRMmQXUokcQU4g55GrSCvyEOlAepF3yFcUh9JQJqqPmqMTUQ7KRUPRaHQGmorOQfPQfHQtWopWoAfROvQ8eh29h7ajr9B+HOBUcCycEc4Wx8HxcOG4RFwKTo5bjCvEleAqcNW4Rlwz7g6uHfca9wVPxDPwbLwt3hMfjI/BC/Fz8Ivxq/Fl+P34OvxF/B18B74P/51AJ+gRbAgeBD4hnpBKmEsoIJQQ9hJqCZcI9whdhI9EIpFFtCC6EYOJCcR04gLiauJ2Yg3xHLGV2EnsJ5FIOiQbkhcpnCQg5ZAKSFtJB0lnSbdJXaTPZBWyIdmRHEhOJEvJy8kl5APkM+Tb5G7yAEWdYkbxoIRTRJT5lHWUPZRGyk1KF2WAqkG1oHpRo6np1GXUUmo19RL1CfW9ioqKsYq7ylQVicpSlVKVwypXVDpUvtA0adY0Hm06TUFbS9tHO0d7SHtPp9PN6b70RHoOfS29kn6B/oz+WZWhaqfKVxWpLlEtV61Tva36Ro2iZqbGVZuplqdWonZM7abaa3WKurk6T12gvli9XP2E+n31fg2GhoNGuEaWxmqNAxpXNXo0SZrmmgGaIs18zd2aFzQ7GTiGCYPHEDJWMPYwLjG6mESmBZPPTGcWMQ8xW5h9WppazlqxWvO0yrVOa7WzcCxzFp+VyVrHOspqY30dpz+OO048btW46nG3x33SHq/tqy3WLtSu0b6n/VWHrROgk6GzQade56kuXtdad6ruXN0dupd0X49njvccLxxfOP7o+Ed6qJ61XqTeAr3dejf0+vUN9IP0Zfpb9S/ovzZgGfgapBtsMjhj0GvIMPQ2lBhuMjxr+JKtxeayM9ml7IvsPiM9o2AjhdEuoxajAWML4xjj5cY1xk9NqCYckxSTTSZNJn2mhqaTTReaVpk+MqOYcczSzLaYNZt9MrcwjzNfaV5v3mOhbcG3yLOosnhiSbf0sZxjWWF514poxbHKsNpudcsatXaxTrMut75pg9q42khsttu0TiBMcJ8gnVAx4b4tzZZrm2tbZdthx7ILs1tuV2/3ZqLpxMSJGyY2T/xu72Kfab/H/rGDpkOIw3KHRod3jtaOQsdyx7tOdKdApyVODU5vnW2cxc47nB+4MFwmu6x0aXL509XNVe5a7drrZuqW5LbN7T6HyYngrOZccSe4+7kvcT/l/sXD1SPH46jHH562nhmeBzx7JllMEk/aM6nTy9hL4LXLq92b7Z3k/ZN3u4+Rj8Cnwue5r4mvyHevbzfXipvOPch942fvJ/er9fvE8+At4p3zx/kH+Rf6twRoBsQElAU8CzQOTA2sCuwLcglaEHQumBAcGrwh+D5fny/kV/L7QtxCFoVcDKWFRoWWhT4Psw6ThzVORieHTN44+ckUsynSKfXhEM4P3xj+NMIiYk7EyanEqRFTy6e+iHSIXBjZHMWImhV1IOpjtF/0uujHMZYxipimWLXY6bGVsZ/i/OOK49rjJ8Yvir+eoJsgSWhIJCXGJu5N7J8WMG3ztK7pLtMLprfNsJgxb8bVmbozM2eenqU2SzDrWBIhKS7pQNI3QbigQtCfzE/eltwn5Am3CF+JfEWbRL1iL3GxuDvFK6U4pSfVK3Vjam+aT1pJ2msJT1ImeZsenL4z/VNGeMa+jMHMuMyaLHJWUtYJqaY0Q3pxtsHsebNbZTayAln7HI85m+f0yUPle7OR7BnZDTlMbDi6obBU/KDoyPXOLc/9PDd27rF5GvOk827Mt56/an53XmDezwvwC4QLmhYaLVy2sGMRd9Guxcji5MVNS0yW5C/pWhq0dP8y6rKMZb8st19evPzDirgVjfn6+UvzO38I+qGqQLVAXnB/pefKnT/if5T82LLKadXWVd8LRYXXiuyLSoq+rRauvrbGYU3pmsG1KWtb1rmu27GeuF66vm2Dz4b9xRrFecWdGydvrNvE3lS46cPmWZuvljiX7NxC3aLY0l4aVtqw1XTr+q3fytLK7pX7ldds09u2atun7aLtt3f47qjeqb+zaOfXnyQ/PdgVtKuuwryiZDdxd+7uF3ti9zT/zPm5cq/u3qK9f+6T7mvfH7n/YqVbZeUBvQPrqtAqRVXvwekHbx3yP9RQbVu9q4ZVU3QYDisOvzySdKTtaOjRpmOcY9XHzY5vq2XUFtYhdfPr+urT6tsbEhpaT4ScaGr0bKw9aXdy3ymjU+WntU6vO0M9k39m8Gze2f5zsnOvz6ee72ya1fT4QvyFuxenXmy5FHrpyuXAyxeauc1nr3hdOXXV4+qJa5xr9dddr9fdcLlR+4vLL7Utri11N91uNtzyv9XYOqn1zG2f2+fv+N+5fJd/9/q9Kfda22LaHtyffr/9gehBz8PMh28f5T4aeLz0CeFJ4VP1pyXP9J5V/Gr1a027a/vpDv+OG8+jnj/uFHa++i37t29d+S/oL0q6Dbsrexx7TvUG9t56Oe1l1yvZq4HXBb9r/L7tjeWb43/4/nGjL76v66387eC71e913u/74PyhqT+i/9nHrI8Dnwo/63ze/4Xzpflr3NfugbnfSN9K/7T6s/F76Pcng1mDgzKBXDA8CuAwRVNSAN7tA6AnADCwGYI6bWSmHhZk5D9gmOA/8cjcPSyuANWYGRqNeOcADmNqvhRAzRdgaCyK9gXUyUmpo/Pv8Kw+JAbYv8K0HECi2x6tebQU/iEjc/xf+v6nBWXWv9l/AV0EC6JTIblRAAAAeGVYSWZNTQAqAAAACAAFARIAAwAAAAEAAQAAARoABQAAAAEAAABKARsABQAAAAEAAABSASgAAwAAAAEAAgAAh2kABAAAAAEAAABaAAAAAAAAAJAAAAABAAAAkAAAAAEAAqACAAQAAAABAAAAFKADAAQAAAABAAAAFAAAAAAXNii1AAAACXBIWXMAABYlAAAWJQFJUiTwAAAB82lUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNi4wLjAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyI+CiAgICAgICAgIDx0aWZmOllSZXNvbHV0aW9uPjE0NDwvdGlmZjpZUmVzb2x1dGlvbj4KICAgICAgICAgPHRpZmY6T3JpZW50YXRpb24+MTwvdGlmZjpPcmllbnRhdGlvbj4KICAgICAgICAgPHRpZmY6WFJlc29sdXRpb24+MTQ0PC90aWZmOlhSZXNvbHV0aW9uPgogICAgICAgICA8dGlmZjpSZXNvbHV0aW9uVW5pdD4yPC90aWZmOlJlc29sdXRpb25Vbml0PgogICAgICA8L3JkZjpEZXNjcmlwdGlvbj4KICAgPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4KReh49gAAAjRJREFUOBGFlD2vMUEUx2clvoNCcW8hCqFAo1dKhEQpvsF9KrWEBh/ALbQ0KkInBI3SWyGPCCJEQliXgsTLefaca/bBWjvJzs6cOf/fnDkzOQJIjWm06/XKBEGgD8c6nU5VIWgBtQDPZPWtJE8O63a7LBgMMo/Hw0ql0jPjcY4RvmqXy4XMjUYDUwLtdhtmsxnYbDbI5/O0djqdFFKmsEiGZ9jP9gem0yn0ej2Yz+fg9XpfycimAD7DttstQTDKfr8Po9GIIg6Hw1Cr1RTgB+A72GAwgMPhQLBMJgNSXsFqtUI2myUo18pA6QJogefsPrLBX4QdCVatViklw+EQRFGEj88P2O12pEUGATmsXq+TaLPZ0AXgMRF2vMEqlQoJTSYTpNNpApvNZliv1/+BHDaZTAi2Wq1A3Ig0xmMej7+RcZjdbodUKkWAaDQK+GHjHPnImB88JrZIJAKFQgH2+z2BOczhcMiwRCIBgUAA+NN5BP6mj2DYff35gk6nA61WCzBn2JxO5wPM7/fLz4vD0E+OECfn8xl/0Gw2KbLxeAyLxQIsFgt8p75pDSO7h/HbpUWpewCike9WLpfB7XaDy+WCYrFI/slk8i0MnRRAUt46hPMI4vE4+Hw+ec7t9/44VgWigEeby+UgFArJWjUYOqhWG6x50rpcSfR6PVUfNOgEVRlTX0HhrZBKz4MZjUYWi8VoA+lc9H/VaRZYjBKrtXR8tlwumcFgeMWRbZpA9ORQWfVm8A/FsrLaxebd5wAAAABJRU5ErkJggg=="; + 'iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAKsGlDQ1BJQ0MgUHJvZmlsZQAASImVlwdUU+kSgOfe9JDQEiIgJfQmSCeAlBBaAAXpYCMkAUKJMRBU7MriClZURLCs6KqIgo0idizYFsWC3QVZBNR1sWDDlXeBQ9jdd9575805c+a7c+efmf+e/z9nLgCdKZDJMlF1gCxpjjwyyI8dn5DIJvUABRiY0kBdIMyWcSMiwgCTUft3+dgGyJC9YzuU69/f/1fREImzhQBIBMbJomxhFsbHMe0TyuQ5ALg9mN9kbo5siK9gzJRjDWL8ZIhTR7hviJOHGY8fjomO5GGsDUCmCQTyVACaKeZn5wpTsTw0f4ztpSKJFGPsGbyzsmaLMMbqgiUWI8N4KD8n+S95Uv+WM1mZUyBIVfLIXoaF7C/JlmUK5v+fn+N/S1amYrSGOaa0NHlwJGaxvpAHGbNDlSxNnhI+yhLRcPwwpymCY0ZZmM1LHGWRwD9UuTZzStgop0gC+co8OfzoURZnB0SNsnx2pLJWipzHHWWBfKyuIiNG6U8T85X589Ki40Y5VxI7ZZSzM6JCx2J4Sr9cEansXywN8hurG6jce1b2X/Yr4SvX5qRFByv3LhjrXyzljuXMjlf2JhL7B4zFxCjjZTl+ylqyzAhlvDgzSOnPzo1Srs3BDuTY2gjlN0wXhESMMoRBELAhBjIhB+QggECQgBTEOeJ5Q2cUeLNl8+WS1LQcNhe7ZWI2Xyq0m8B2tHd0Bhi6syNH4j1r+C4irGtjvhWVAF4nBgcHT475Qm4BHEkCoNaO+SxnAKh3A1w5JVTIc0d8Q9cJCEAFNWCCDhiACViCLTiCK3iCLwRACIRDNCTATBBCGmRhnc+FhbAMCqAI1sNmKIOdsBv2wyE4CvVwCs7DZbgOt+AePIZ26IJX0AcfYQBBEBJCRxiIDmKImCE2iCPCQbyRACQMiUQSkCQkFZEiCmQhsgIpQoqRMmQXUokcQU4g55GrSCvyEOlAepF3yFcUh9JQJqqPmqMTUQ7KRUPRaHQGmorOQfPQfHQtWopWoAfROvQ8eh29h7ajr9B+HOBUcCycEc4Wx8HxcOG4RFwKTo5bjCvEleAqcNW4Rlwz7g6uHfca9wVPxDPwbLwt3hMfjI/BC/Fz8Ivxq/Fl+P34OvxF/B18B74P/51AJ+gRbAgeBD4hnpBKmEsoIJQQ9hJqCZcI9whdhI9EIpFFtCC6EYOJCcR04gLiauJ2Yg3xHLGV2EnsJ5FIOiQbkhcpnCQg5ZAKSFtJB0lnSbdJXaTPZBWyIdmRHEhOJEvJy8kl5APkM+Tb5G7yAEWdYkbxoIRTRJT5lHWUPZRGyk1KF2WAqkG1oHpRo6np1GXUUmo19RL1CfW9ioqKsYq7ylQVicpSlVKVwypXVDpUvtA0adY0Hm06TUFbS9tHO0d7SHtPp9PN6b70RHoOfS29kn6B/oz+WZWhaqfKVxWpLlEtV61Tva36Ro2iZqbGVZuplqdWonZM7abaa3WKurk6T12gvli9XP2E+n31fg2GhoNGuEaWxmqNAxpXNXo0SZrmmgGaIs18zd2aFzQ7GTiGCYPHEDJWMPYwLjG6mESmBZPPTGcWMQ8xW5h9WppazlqxWvO0yrVOa7WzcCxzFp+VyVrHOspqY30dpz+OO048btW46nG3x33SHq/tqy3WLtSu0b6n/VWHrROgk6GzQade56kuXtdad6ruXN0dupd0X49njvccLxxfOP7o+Ed6qJ61XqTeAr3dejf0+vUN9IP0Zfpb9S/ovzZgGfgapBtsMjhj0GvIMPQ2lBhuMjxr+JKtxeayM9ml7IvsPiM9o2AjhdEuoxajAWML4xjj5cY1xk9NqCYckxSTTSZNJn2mhqaTTReaVpk+MqOYcczSzLaYNZt9MrcwjzNfaV5v3mOhbcG3yLOosnhiSbf0sZxjWWF514poxbHKsNpudcsatXaxTrMut75pg9q42khsttu0TiBMcJ8gnVAx4b4tzZZrm2tbZdthx7ILs1tuV2/3ZqLpxMSJGyY2T/xu72Kfab/H/rGDpkOIw3KHRod3jtaOQsdyx7tOdKdApyVODU5vnW2cxc47nB+4MFwmu6x0aXL509XNVe5a7drrZuqW5LbN7T6HyYngrOZccSe4+7kvcT/l/sXD1SPH46jHH562nhmeBzx7JllMEk/aM6nTy9hL4LXLq92b7Z3k/ZN3u4+Rj8Cnwue5r4mvyHevbzfXipvOPch942fvJ/er9fvE8+At4p3zx/kH+Rf6twRoBsQElAU8CzQOTA2sCuwLcglaEHQumBAcGrwh+D5fny/kV/L7QtxCFoVcDKWFRoWWhT4Psw6ThzVORieHTN44+ckUsynSKfXhEM4P3xj+NMIiYk7EyanEqRFTy6e+iHSIXBjZHMWImhV1IOpjtF/0uujHMZYxipimWLXY6bGVsZ/i/OOK49rjJ8Yvir+eoJsgSWhIJCXGJu5N7J8WMG3ztK7pLtMLprfNsJgxb8bVmbozM2eenqU2SzDrWBIhKS7pQNI3QbigQtCfzE/eltwn5Am3CF+JfEWbRL1iL3GxuDvFK6U4pSfVK3Vjam+aT1pJ2msJT1ImeZsenL4z/VNGeMa+jMHMuMyaLHJWUtYJqaY0Q3pxtsHsebNbZTayAln7HI85m+f0yUPle7OR7BnZDTlMbDi6obBU/KDoyPXOLc/9PDd27rF5GvOk827Mt56/an53XmDezwvwC4QLmhYaLVy2sGMRd9Guxcji5MVNS0yW5C/pWhq0dP8y6rKMZb8st19evPzDirgVjfn6+UvzO38I+qGqQLVAXnB/pefKnT/if5T82LLKadXWVd8LRYXXiuyLSoq+rRauvrbGYU3pmsG1KWtb1rmu27GeuF66vm2Dz4b9xRrFecWdGydvrNvE3lS46cPmWZuvljiX7NxC3aLY0l4aVtqw1XTr+q3fytLK7pX7ldds09u2atun7aLtt3f47qjeqb+zaOfXnyQ/PdgVtKuuwryiZDdxd+7uF3ti9zT/zPm5cq/u3qK9f+6T7mvfH7n/YqVbZeUBvQPrqtAqRVXvwekHbx3yP9RQbVu9q4ZVU3QYDisOvzySdKTtaOjRpmOcY9XHzY5vq2XUFtYhdfPr+urT6tsbEhpaT4ScaGr0bKw9aXdy3ymjU+WntU6vO0M9k39m8Gze2f5zsnOvz6ee72ya1fT4QvyFuxenXmy5FHrpyuXAyxeauc1nr3hdOXXV4+qJa5xr9dddr9fdcLlR+4vLL7Utri11N91uNtzyv9XYOqn1zG2f2+fv+N+5fJd/9/q9Kfda22LaHtyffr/9gehBz8PMh28f5T4aeLz0CeFJ4VP1pyXP9J5V/Gr1a027a/vpDv+OG8+jnj/uFHa++i37t29d+S/oL0q6Dbsrexx7TvUG9t56Oe1l1yvZq4HXBb9r/L7tjeWb43/4/nGjL76v66387eC71e913u/74PyhqT+i/9nHrI8Dnwo/63ze/4Xzpflr3NfugbnfSN9K/7T6s/F76Pcng1mDgzKBXDA8CuAwRVNSAN7tA6AnADCwGYI6bWSmHhZk5D9gmOA/8cjcPSyuANWYGRqNeOcADmNqvhRAzRdgaCyK9gXUyUmpo/Pv8Kw+JAbYv8K0HECi2x6tebQU/iEjc/xf+v6nBWXWv9l/AV0EC6JTIblRAAAAeGVYSWZNTQAqAAAACAAFARIAAwAAAAEAAQAAARoABQAAAAEAAABKARsABQAAAAEAAABSASgAAwAAAAEAAgAAh2kABAAAAAEAAABaAAAAAAAAAJAAAAABAAAAkAAAAAEAAqACAAQAAAABAAAAFKADAAQAAAABAAAAFAAAAAAXNii1AAAACXBIWXMAABYlAAAWJQFJUiTwAAAB82lUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNi4wLjAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyI+CiAgICAgICAgIDx0aWZmOllSZXNvbHV0aW9uPjE0NDwvdGlmZjpZUmVzb2x1dGlvbj4KICAgICAgICAgPHRpZmY6T3JpZW50YXRpb24+MTwvdGlmZjpPcmllbnRhdGlvbj4KICAgICAgICAgPHRpZmY6WFJlc29sdXRpb24+MTQ0PC90aWZmOlhSZXNvbHV0aW9uPgogICAgICAgICA8dGlmZjpSZXNvbHV0aW9uVW5pdD4yPC90aWZmOlJlc29sdXRpb25Vbml0PgogICAgICA8L3JkZjpEZXNjcmlwdGlvbj4KICAgPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4KReh49gAAAjRJREFUOBGFlD2vMUEUx2clvoNCcW8hCqFAo1dKhEQpvsF9KrWEBh/ALbQ0KkInBI3SWyGPCCJEQliXgsTLefaca/bBWjvJzs6cOf/fnDkzOQJIjWm06/XKBEGgD8c6nU5VIWgBtQDPZPWtJE8O63a7LBgMMo/Hw0ql0jPjcY4RvmqXy4XMjUYDUwLtdhtmsxnYbDbI5/O0djqdFFKmsEiGZ9jP9gem0yn0ej2Yz+fg9XpfycimAD7DttstQTDKfr8Po9GIIg6Hw1Cr1RTgB+A72GAwgMPhQLBMJgNSXsFqtUI2myUo18pA6QJogefsPrLBX4QdCVatViklw+EQRFGEj88P2O12pEUGATmsXq+TaLPZ0AXgMRF2vMEqlQoJTSYTpNNpApvNZliv1/+BHDaZTAi2Wq1A3Ig0xmMej7+RcZjdbodUKkWAaDQK+GHjHPnImB88JrZIJAKFQgH2+z2BOczhcMiwRCIBgUAA+NN5BP6mj2DYff35gk6nA61WCzBn2JxO5wPM7/fLz4vD0E+OECfn8xl/0Gw2KbLxeAyLxQIsFgt8p75pDSO7h/HbpUWpewCike9WLpfB7XaDy+WCYrFI/slk8i0MnRRAUt46hPMI4vE4+Hw+ec7t9/44VgWigEeby+UgFArJWjUYOqhWG6x50rpcSfR6PVUfNOgEVRlTX0HhrZBKz4MZjUYWi8VoA+lc9H/VaRZYjBKrtXR8tlwumcFgeMWRbZpA9ORQWfVm8A/FsrLaxebd5wAAAABJRU5ErkJggg=='; // Tool configuration -const name = "get-tiny-image"; +const name = 'get-tiny-image'; const config = { - title: "Get Tiny Image Tool", - description: "Returns a tiny MCP logo image.", - inputSchema: {}, - annotations: { - readOnlyHint: true, - destructiveHint: false, - idempotentHint: true, - openWorldHint: false, - }, + title: 'Get Tiny Image Tool', + description: 'Returns a tiny MCP logo image.', + inputSchema: {}, + annotations: { + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: false + } }; /** @@ -31,23 +30,23 @@ const config = { * @param server - The McpServer instance where the tool will be registered. */ export const registerGetTinyImageTool = (server: McpServer) => { - server.registerTool(name, config, async (args): Promise => { - return { - content: [ - { - type: "text", - text: "Here's the image you requested:", - }, - { - type: "image", - data: MCP_TINY_IMAGE, - mimeType: "image/png", - }, - { - type: "text", - text: "The image above is the MCP logo.", - }, - ], - }; - }); + server.registerTool(name, config, async (args): Promise => { + return { + content: [ + { + type: 'text', + text: "Here's the image you requested:" + }, + { + type: 'image', + data: MCP_TINY_IMAGE, + mimeType: 'image/png' + }, + { + type: 'text', + text: 'The image above is the MCP logo.' + } + ] + }; + }); }; diff --git a/src/everything/tools/gzip-file-as-resource.ts b/src/everything/tools/gzip-file-as-resource.ts index 3dd6fdae4a..00e0002ce6 100644 --- a/src/everything/tools/gzip-file-as-resource.ts +++ b/src/everything/tools/gzip-file-as-resource.ts @@ -1,58 +1,48 @@ -import { z } from "zod"; -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { CallToolResult, Resource } from "@modelcontextprotocol/sdk/types.js"; -import { gzipSync } from "node:zlib"; -import { - getSessionResourceURI, - registerSessionResource, -} from "../resources/session.js"; +import { z } from 'zod'; +import { McpServer, CallToolResult, Resource } from '@modelcontextprotocol/server'; +import { gzipSync } from 'node:zlib'; +import { getSessionResourceURI, registerSessionResource } from '../resources/session.js'; // Maximum input file size - 10 MB default -const GZIP_MAX_FETCH_SIZE = Number( - process.env.GZIP_MAX_FETCH_SIZE ?? String(10 * 1024 * 1024) -); +const GZIP_MAX_FETCH_SIZE = Number(process.env.GZIP_MAX_FETCH_SIZE ?? String(10 * 1024 * 1024)); // Maximum fetch time - 30 seconds default. -const GZIP_MAX_FETCH_TIME_MILLIS = Number( - process.env.GZIP_MAX_FETCH_TIME_MILLIS ?? String(30 * 1000) -); +const GZIP_MAX_FETCH_TIME_MILLIS = Number(process.env.GZIP_MAX_FETCH_TIME_MILLIS ?? String(30 * 1000)); // Comma-separated list of allowed domains. Empty means all domains are allowed. -const GZIP_ALLOWED_DOMAINS = (process.env.GZIP_ALLOWED_DOMAINS ?? "") - .split(",") - .map((d) => d.trim().toLowerCase()) - .filter((d) => d.length > 0); +const GZIP_ALLOWED_DOMAINS = (process.env.GZIP_ALLOWED_DOMAINS ?? '') + .split(',') + .map(d => d.trim().toLowerCase()) + .filter(d => d.length > 0); // Tool input schema const GZipFileAsResourceSchema = z.object({ - name: z.string().describe("Name of the output file").default("README.md.gz"), - data: z - .url() - .describe("URL or data URI of the file content to compress") - .default( - "https://raw.githubusercontent.com/modelcontextprotocol/servers/refs/heads/main/README.md" - ), - outputType: z - .enum(["resourceLink", "resource"]) - .default("resourceLink") - .describe( - "How the resulting gzipped file should be returned. 'resourceLink' returns a link to a resource that can be read later, 'resource' returns a full resource object." - ), + name: z.string().describe('Name of the output file').default('README.md.gz'), + data: z + .url() + .describe('URL or data URI of the file content to compress') + .default('https://raw.githubusercontent.com/modelcontextprotocol/servers/refs/heads/main/README.md'), + outputType: z + .enum(['resourceLink', 'resource']) + .default('resourceLink') + .describe( + "How the resulting gzipped file should be returned. 'resourceLink' returns a link to a resource that can be read later, 'resource' returns a full resource object." + ) }); // Tool configuration -const name = "gzip-file-as-resource"; +const name = 'gzip-file-as-resource'; const config = { - title: "GZip File as Resource Tool", - description: - "Compresses a single file using gzip compression. Depending upon the selected output type, returns either the compressed data as a gzipped resource or a resource link, allowing it to be downloaded in a subsequent request during the current session.", - inputSchema: GZipFileAsResourceSchema, - annotations: { - readOnlyHint: false, - destructiveHint: false, - idempotentHint: true, - openWorldHint: true, - }, + title: 'GZip File as Resource Tool', + description: + 'Compresses a single file using gzip compression. Depending upon the selected output type, returns either the compressed data as a gzipped resource or a resource link, allowing it to be downloaded in a subsequent request during the current session.', + inputSchema: GZipFileAsResourceSchema, + annotations: { + readOnlyHint: false, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true + } }; /** @@ -71,58 +61,49 @@ const config = { * @throws {Error} Throws an error if an unknown output type is specified. */ export const registerGZipFileAsResourceTool = (server: McpServer) => { - server.registerTool(name, config, async (args): Promise => { - const { - name, - data: dataUri, - outputType, - } = GZipFileAsResourceSchema.parse(args); - - // Validate data uri - const url = validateDataURI(dataUri); - - // Fetch the data - const response = await fetchSafely(url, { - maxBytes: GZIP_MAX_FETCH_SIZE, - timeoutMillis: GZIP_MAX_FETCH_TIME_MILLIS, + server.registerTool(name, config, async (args): Promise => { + const { name, data: dataUri, outputType } = GZipFileAsResourceSchema.parse(args); + + // Validate data uri + const url = validateDataURI(dataUri); + + // Fetch the data + const response = await fetchSafely(url, { + maxBytes: GZIP_MAX_FETCH_SIZE, + timeoutMillis: GZIP_MAX_FETCH_TIME_MILLIS + }); + + // Compress the data using gzip + const inputBuffer = Buffer.from(response); + const compressedBuffer = gzipSync(inputBuffer); + + // Create resource + const uri = getSessionResourceURI(name); + const blob = compressedBuffer.toString('base64'); + const mimeType = 'application/gzip'; + const resource = { uri, name, mimeType }; + + // Register resource, get resource link in return + const resourceLink = registerSessionResource(server, resource, 'blob', blob); + + // Return the resource or a resource link that can be used to access this resource later + if (outputType === 'resource') { + return { + content: [ + { + type: 'resource', + resource: { uri, mimeType, blob } + } + ] + }; + } else if (outputType === 'resourceLink') { + return { + content: [resourceLink] + }; + } else { + throw new Error(`Unknown outputType: ${outputType}`); + } }); - - // Compress the data using gzip - const inputBuffer = Buffer.from(response); - const compressedBuffer = gzipSync(inputBuffer); - - // Create resource - const uri = getSessionResourceURI(name); - const blob = compressedBuffer.toString("base64"); - const mimeType = "application/gzip"; - const resource = { uri, name, mimeType }; - - // Register resource, get resource link in return - const resourceLink = registerSessionResource( - server, - resource, - "blob", - blob - ); - - // Return the resource or a resource link that can be used to access this resource later - if (outputType === "resource") { - return { - content: [ - { - type: "resource", - resource: { uri, mimeType, blob }, - }, - ], - }; - } else if (outputType === "resourceLink") { - return { - content: [resourceLink], - }; - } else { - throw new Error(`Unknown outputType: ${outputType}`); - } - }); }; /** @@ -133,38 +114,25 @@ export const registerGZipFileAsResourceTool = (server: McpServer) => { * @throws {Error} If the data URI does not use a supported protocol or does not meet allowed domains criteria. */ function validateDataURI(dataUri: string): URL { - // Validate Inputs - const url = new URL(dataUri); - try { - if ( - url.protocol !== "http:" && - url.protocol !== "https:" && - url.protocol !== "data:" - ) { - throw new Error( - `Unsupported URL protocol for ${dataUri}. Only http, https, and data URLs are supported.` - ); - } - if ( - GZIP_ALLOWED_DOMAINS.length > 0 && - (url.protocol === "http:" || url.protocol === "https:") - ) { - const domain = url.hostname; - const domainAllowed = GZIP_ALLOWED_DOMAINS.some((allowedDomain) => { - return domain === allowedDomain || domain.endsWith(`.${allowedDomain}`); - }); - if (!domainAllowed) { - throw new Error(`Domain ${domain} is not in the allowed domains list.`); - } + // Validate Inputs + const url = new URL(dataUri); + try { + if (url.protocol !== 'http:' && url.protocol !== 'https:' && url.protocol !== 'data:') { + throw new Error(`Unsupported URL protocol for ${dataUri}. Only http, https, and data URLs are supported.`); + } + if (GZIP_ALLOWED_DOMAINS.length > 0 && (url.protocol === 'http:' || url.protocol === 'https:')) { + const domain = url.hostname; + const domainAllowed = GZIP_ALLOWED_DOMAINS.some(allowedDomain => { + return domain === allowedDomain || domain.endsWith(`.${allowedDomain}`); + }); + if (!domainAllowed) { + throw new Error(`Domain ${domain} is not in the allowed domains list.`); + } + } + } catch (error) { + throw new Error(`Error processing file ${dataUri}: ${error instanceof Error ? error.message : String(error)}`); } - } catch (error) { - throw new Error( - `Error processing file ${dataUri}: ${ - error instanceof Error ? error.message : String(error) - }` - ); - } - return url; + return url; } /** @@ -177,72 +145,64 @@ function validateDataURI(dataUri: string): URL { * @return {Promise} A promise that resolves with the response as an ArrayBuffer if successful. * @throws {Error} Throws an error if the response size exceeds the defined limit, the fetch times out, or the response is otherwise invalid. */ -async function fetchSafely( - url: URL, - { maxBytes, timeoutMillis }: { maxBytes: number; timeoutMillis: number } -): Promise { - const controller = new AbortController(); - const timeout = setTimeout( - () => - controller.abort( - `Fetching ${url} took more than ${timeoutMillis} ms and was aborted.` - ), - timeoutMillis - ); - - try { - // Fetch the data - const response = await fetch(url, { signal: controller.signal }); - if (!response.body) { - throw new Error("No response body"); - } +async function fetchSafely(url: URL, { maxBytes, timeoutMillis }: { maxBytes: number; timeoutMillis: number }): Promise { + const controller = new AbortController(); + const timeout = setTimeout( + () => controller.abort(`Fetching ${url} took more than ${timeoutMillis} ms and was aborted.`), + timeoutMillis + ); - // Note: we can't trust the Content-Length header: a malicious or clumsy server could return much more data than advertised. - // We check it here for early bail-out, but we still need to monitor actual bytes read below. - const contentLengthHeader = response.headers.get("content-length"); - if (contentLengthHeader != null) { - const contentLength = parseInt(contentLengthHeader, 10); - if (contentLength > maxBytes) { - throw new Error( - `Content-Length for ${url} exceeds max of ${maxBytes}: ${contentLength}` - ); - } - } + try { + // Fetch the data + const response = await fetch(url, { signal: controller.signal }); + if (!response.body) { + throw new Error('No response body'); + } - // Read the fetched data from the response body - const reader = response.body.getReader(); - const chunks = []; - let totalSize = 0; + // Note: we can't trust the Content-Length header: a malicious or clumsy server could return much more data than advertised. + // We check it here for early bail-out, but we still need to monitor actual bytes read below. + const contentLengthHeader = response.headers.get('content-length'); + if (contentLengthHeader != null) { + const contentLength = parseInt(contentLengthHeader, 10); + if (contentLength > maxBytes) { + throw new Error(`Content-Length for ${url} exceeds max of ${maxBytes}: ${contentLength}`); + } + } - // Read chunks until done - try { - while (true) { - const { done, value } = await reader.read(); - if (done) break; + // Read the fetched data from the response body + const reader = response.body.getReader(); + const chunks = []; + let totalSize = 0; + + // Read chunks until done + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; - totalSize += value.length; + totalSize += value.length; - if (totalSize > maxBytes) { - reader.cancel(); - throw new Error(`Response from ${url} exceeds ${maxBytes} bytes`); + if (totalSize > maxBytes) { + reader.cancel(); + throw new Error(`Response from ${url} exceeds ${maxBytes} bytes`); + } + + chunks.push(value); + } + } finally { + reader.releaseLock(); } - chunks.push(value); - } - } finally { - reader.releaseLock(); - } + // Combine chunks into a single buffer + const buffer = new Uint8Array(totalSize); + let offset = 0; + for (const chunk of chunks) { + buffer.set(chunk, offset); + offset += chunk.length; + } - // Combine chunks into a single buffer - const buffer = new Uint8Array(totalSize); - let offset = 0; - for (const chunk of chunks) { - buffer.set(chunk, offset); - offset += chunk.length; + return buffer.buffer; + } finally { + clearTimeout(timeout); } - - return buffer.buffer; - } finally { - clearTimeout(timeout); - } } diff --git a/src/everything/tools/index.ts b/src/everything/tools/index.ts index 0272126eea..c6dd555208 100644 --- a/src/everything/tools/index.ts +++ b/src/everything/tools/index.ts @@ -1,41 +1,40 @@ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { registerGetAnnotatedMessageTool } from "./get-annotated-message.js"; -import { registerEchoTool } from "./echo.js"; -import { registerGetEnvTool } from "./get-env.js"; -import { registerGetResourceLinksTool } from "./get-resource-links.js"; -import { registerGetResourceReferenceTool } from "./get-resource-reference.js"; -import { registerGetRootsListTool } from "./get-roots-list.js"; -import { registerGetStructuredContentTool } from "./get-structured-content.js"; -import { registerGetSumTool } from "./get-sum.js"; -import { registerGetTinyImageTool } from "./get-tiny-image.js"; -import { registerGZipFileAsResourceTool } from "./gzip-file-as-resource.js"; -import { registerToggleSimulatedLoggingTool } from "./toggle-simulated-logging.js"; -import { registerToggleSubscriberUpdatesTool } from "./toggle-subscriber-updates.js"; -import { registerTriggerElicitationRequestTool } from "./trigger-elicitation-request.js"; -import { registerTriggerLongRunningOperationTool } from "./trigger-long-running-operation.js"; -import { registerTriggerSamplingRequestTool } from "./trigger-sampling-request.js"; -import { registerTriggerSamplingRequestAsyncTool } from "./trigger-sampling-request-async.js"; -import { registerTriggerElicitationRequestAsyncTool } from "./trigger-elicitation-request-async.js"; -import { registerSimulateResearchQueryTool } from "./simulate-research-query.js"; -import { registerTriggerUrlElicitationTool } from "./trigger-url-elicitation.js"; +import { McpServer } from '@modelcontextprotocol/server'; +import { registerGetAnnotatedMessageTool } from './get-annotated-message.js'; +import { registerEchoTool } from './echo.js'; +import { registerGetEnvTool } from './get-env.js'; +import { registerGetResourceLinksTool } from './get-resource-links.js'; +import { registerGetResourceReferenceTool } from './get-resource-reference.js'; +import { registerGetRootsListTool } from './get-roots-list.js'; +import { registerGetStructuredContentTool } from './get-structured-content.js'; +import { registerGetSumTool } from './get-sum.js'; +import { registerGetTinyImageTool } from './get-tiny-image.js'; +import { registerGZipFileAsResourceTool } from './gzip-file-as-resource.js'; +import { registerToggleSimulatedLoggingTool } from './toggle-simulated-logging.js'; +import { registerToggleSubscriberUpdatesTool } from './toggle-subscriber-updates.js'; +import { registerTriggerElicitationRequestTool } from './trigger-elicitation-request.js'; +import { registerTriggerLongRunningOperationTool } from './trigger-long-running-operation.js'; +import { registerTriggerSamplingRequestTool } from './trigger-sampling-request.js'; +import { registerTriggerSamplingRequestAsyncTool } from './trigger-sampling-request-async.js'; +import { registerTriggerElicitationRequestAsyncTool } from './trigger-elicitation-request-async.js'; +import { registerTriggerUrlElicitationTool } from './trigger-url-elicitation.js'; /** * Register the tools with the MCP server. * @param server */ export const registerTools = (server: McpServer) => { - registerEchoTool(server); - registerGetAnnotatedMessageTool(server); - registerGetEnvTool(server); - registerGetResourceLinksTool(server); - registerGetResourceReferenceTool(server); - registerGetStructuredContentTool(server); - registerGetSumTool(server); - registerGetTinyImageTool(server); - registerGZipFileAsResourceTool(server); - registerToggleSimulatedLoggingTool(server); - registerToggleSubscriberUpdatesTool(server); - registerTriggerLongRunningOperationTool(server); + registerEchoTool(server); + registerGetAnnotatedMessageTool(server); + registerGetEnvTool(server); + registerGetResourceLinksTool(server); + registerGetResourceReferenceTool(server); + registerGetStructuredContentTool(server); + registerGetSumTool(server); + registerGetTinyImageTool(server); + registerGZipFileAsResourceTool(server); + registerToggleSimulatedLoggingTool(server); + registerToggleSubscriberUpdatesTool(server); + registerTriggerLongRunningOperationTool(server); }; /** @@ -43,13 +42,11 @@ export const registerTools = (server: McpServer) => { * These must be registered conditionally, after initialization. */ export const registerConditionalTools = (server: McpServer) => { - registerGetRootsListTool(server); - registerTriggerElicitationRequestTool(server); - registerTriggerUrlElicitationTool(server); - registerTriggerSamplingRequestTool(server); - // Task-based research tool (uses experimental tasks API) - registerSimulateResearchQueryTool(server); - // Bidirectional task tools - server sends requests that client executes as tasks - registerTriggerSamplingRequestAsyncTool(server); - registerTriggerElicitationRequestAsyncTool(server); + registerGetRootsListTool(server); + registerTriggerElicitationRequestTool(server); + registerTriggerUrlElicitationTool(server); + registerTriggerSamplingRequestTool(server); + // Bidirectional task tools - server sends requests that client executes as tasks + registerTriggerSamplingRequestAsyncTool(server); + registerTriggerElicitationRequestAsyncTool(server); }; diff --git a/src/everything/tools/simulate-research-query.ts b/src/everything/tools/simulate-research-query.ts deleted file mode 100644 index 098df8a0a0..0000000000 --- a/src/everything/tools/simulate-research-query.ts +++ /dev/null @@ -1,345 +0,0 @@ -import { z } from "zod"; -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { - CallToolResult, - GetTaskResult, - Task, - ElicitResult, - ElicitResultSchema, -} from "@modelcontextprotocol/sdk/types.js"; -import { CreateTaskResult } from "@modelcontextprotocol/sdk/experimental/tasks"; - -// Tool input schema -const SimulateResearchQuerySchema = z.object({ - topic: z.string().describe("The research topic to investigate"), - ambiguous: z - .boolean() - .default(false) - .describe( - "Simulate an ambiguous query that requires clarification (triggers input_required status)" - ), -}); - -// Research stages -const STAGES = [ - "Gathering sources", - "Analyzing content", - "Synthesizing findings", - "Generating report", -]; - -// Duration per stage in milliseconds -const STAGE_DURATION = 1000; - -// Internal state for tracking research tasks -interface ResearchState { - topic: string; - ambiguous: boolean; - currentStage: number; - clarification?: string; - completed: boolean; - result?: CallToolResult; -} - -// Map to store research state per task -const researchStates = new Map(); - -/** - * Runs the background research process. - * Updates task status as it progresses through stages. - * If clarification is needed, sends elicitation via sendRequest with relatedTask, - * which queues the request in the task message queue. The SDK delivers it through - * the tasks/result stream when the client calls tasks/result (per spec input_required flow). - * This works on all transports (STDIO, SSE, Streamable HTTP). - */ -async function runResearchProcess( - taskId: string, - args: z.infer, - taskStore: { - updateTaskStatus: ( - taskId: string, - status: Task["status"], - message?: string - ) => Promise; - storeTaskResult: ( - taskId: string, - status: "completed" | "failed", - result: CallToolResult - ) => Promise; - }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - sendRequest: any -): Promise { - const state = researchStates.get(taskId); - if (!state) return; - - // Process each stage - for (let i = state.currentStage; i < STAGES.length; i++) { - state.currentStage = i; - - // Check if task was cancelled externally - if (state.completed) return; - - // Update status message for current stage - await taskStore.updateTaskStatus(taskId, "working", `${STAGES[i]}...`); - - // At synthesis stage (index 2), check if clarification is needed - if (i === 2 && state.ambiguous && !state.clarification) { - // Update status to show we're requesting input (spec SHOULD) - await taskStore.updateTaskStatus( - taskId, - "input_required", - `Found multiple interpretations for "${state.topic}". Requesting clarification...` - ); - - try { - // relatedTask queues elicitation via task message queue → delivered through tasks/result on all transports - const elicitResult: ElicitResult = await sendRequest( - { - method: "elicitation/create", - params: { - message: `The research query "${state.topic}" could have multiple interpretations. Please clarify what you're looking for:`, - requestedSchema: { - type: "object", - properties: { - interpretation: { - type: "string", - title: "Clarification", - description: - "Which interpretation of the topic do you mean?", - oneOf: getInterpretationsForTopic(state.topic), - }, - }, - required: ["interpretation"], - }, - }, - }, - ElicitResultSchema, - { relatedTask: { taskId } } - ); - - // Process elicitation response - if (elicitResult.action === "accept" && elicitResult.content) { - state.clarification = - (elicitResult.content as { interpretation?: string }) - .interpretation || "User accepted without selection"; - } else if (elicitResult.action === "decline") { - state.clarification = "User declined - using default interpretation"; - } else { - state.clarification = "User cancelled - using default interpretation"; - } - } catch (error) { - // Elicitation failed - use default interpretation and continue - console.warn( - `Elicitation failed for task ${taskId}:`, - error instanceof Error ? error.message : String(error) - ); - state.clarification = "technical (default - elicitation unavailable)"; - } - - // Resume with working status (spec SHOULD) - await taskStore.updateTaskStatus( - taskId, - "working", - `Continuing with interpretation: "${state.clarification}"...` - ); - - // Continue processing (no return - just keep going through the loop) - } - - // Simulate work for this stage - await new Promise((resolve) => setTimeout(resolve, STAGE_DURATION)); - } - - // All stages complete - generate result - state.completed = true; - const result = generateResearchReport(state); - state.result = result; - - await taskStore.storeTaskResult(taskId, "completed", result); -} - -/** - * Generates the final research report with educational content about tasks. - */ -function generateResearchReport(state: ResearchState): CallToolResult { - const topic = state.clarification - ? `${state.topic} (${state.clarification})` - : state.topic; - - const report = `# Research Report: ${topic} - -## Research Parameters -- **Topic**: ${state.topic} -${state.clarification ? `- **Clarification**: ${state.clarification}` : ""} - -## Synthesis -This research query was processed through ${STAGES.length} stages: -${STAGES.map((s, i) => `- Stage ${i + 1}: ${s} ✓`).join("\n")} - ---- - -## About This Demo (SEP-1686: Tasks) - -This tool demonstrates MCP's task-based execution pattern for long-running operations: - -**Task Lifecycle Demonstrated:** -1. \`tools/call\` with \`task\` parameter → Server returns \`CreateTaskResult\` (not the final result) -2. Client polls \`tasks/get\` → Server returns current status and \`statusMessage\` -3. Status progressed: \`working\` → ${ - state.clarification ? `\`input_required\` → \`working\` → ` : "" - }\`completed\` -4. Client calls \`tasks/result\` → Server returns this final result - -${ - state.clarification - ? `**Elicitation Flow:** -When the query was ambiguous, the server sent an \`elicitation/create\` request -to the client. The task status changed to \`input_required\` while awaiting user input. -${ - state.clarification.includes("unavailable") - ? `**Note:** Elicitation failed and a default interpretation was used.` - : `After receiving clarification ("${state.clarification}"), the task resumed processing and completed.` -} -` - : "" -} -**Key Concepts:** -- Tasks enable "call now, fetch later" patterns -- \`statusMessage\` provides human-readable progress updates -- Tasks have TTL (time-to-live) for automatic cleanup -- \`pollInterval\` suggests how often to check status -- Elicitation requests use \`relatedTask\` to queue via tasks/result (works on all transports) - -*This is a simulated research report from the Everything MCP Server.* -`; - - return { - content: [ - { - type: "text", - text: report, - }, - ], - }; -} - -/** - * Registers the 'simulate-research-query' tool as a task-based tool. - * - * This tool demonstrates the MCP Tasks feature (SEP-1686) with a real-world scenario: - * a research tool that gathers and synthesizes information from multiple sources. - * If the query is ambiguous, it pauses to ask for clarification before completing. - * - * @param {McpServer} server - The McpServer instance where the tool will be registered. - */ -export const registerSimulateResearchQueryTool = (server: McpServer) => { - // Check if client supports elicitation (needed for input_required flow) - const clientCapabilities = server.server.getClientCapabilities() || {}; - const clientSupportsElicitation: boolean = - clientCapabilities.elicitation !== undefined; - - server.experimental.tasks.registerToolTask( - "simulate-research-query", - { - title: "Simulate Research Query", - description: - "Simulates a deep research operation that gathers, analyzes, and synthesizes information. " + - "Demonstrates MCP task-based operations with progress through multiple stages. " + - "If 'ambiguous' is true and client supports elicitation, sends an elicitation request for clarification.", - inputSchema: SimulateResearchQuerySchema, - execution: { taskSupport: "required" }, - annotations: { - readOnlyHint: false, - destructiveHint: false, - idempotentHint: false, - openWorldHint: false, - }, - }, - { - /** - * Creates a new research task and starts background processing. - */ - createTask: async (args, extra): Promise => { - const validatedArgs = SimulateResearchQuerySchema.parse(args); - - // Create the task in the store - const task = await extra.taskStore.createTask({ - ttl: 300000, // 5 minutes - pollInterval: 1000, - }); - - // Initialize research state - const state: ResearchState = { - topic: validatedArgs.topic, - ambiguous: validatedArgs.ambiguous && clientSupportsElicitation, - currentStage: 0, - completed: false, - }; - researchStates.set(task.taskId, state); - - // Start background research (don't await - runs asynchronously) - // Pass sendRequest for elicitation (queued via task message queue, works on all transports) - runResearchProcess( - task.taskId, - validatedArgs, - extra.taskStore, - extra.sendRequest - ).catch((error) => { - console.error(`Research task ${task.taskId} failed:`, error); - extra.taskStore - .updateTaskStatus(task.taskId, "failed", String(error)) - .catch(console.error); - }); - - return { task }; - }, - - /** - * Returns the current status of the research task. - */ - getTask: async (args, extra): Promise => { - return await extra.taskStore.getTask(extra.taskId); - }, - - /** - * Returns the task result. - * Elicitation is now handled directly in the background process. - */ - getTaskResult: async (args, extra): Promise => { - // Return the stored result - const result = await extra.taskStore.getTaskResult(extra.taskId); - - // Clean up state - researchStates.delete(extra.taskId); - - return result as CallToolResult; - }, - } - ); -}; - -/** - * Returns contextual interpretation options based on the topic. - */ -function getInterpretationsForTopic( - topic: string -): Array<{ const: string; title: string }> { - const lowerTopic = topic.toLowerCase(); - - // Example: contextual interpretations for "python" - if (lowerTopic.includes("python")) { - return [ - { const: "programming", title: "Python programming language" }, - { const: "snake", title: "Python snake species" }, - { const: "comedy", title: "Monty Python comedy group" }, - ]; - } - - // Default generic interpretations - return [ - { const: "technical", title: "Technical/scientific perspective" }, - { const: "historical", title: "Historical perspective" }, - { const: "current", title: "Current events/news perspective" }, - ]; -} diff --git a/src/everything/tools/toggle-simulated-logging.ts b/src/everything/tools/toggle-simulated-logging.ts index eb9a4b1e25..480c72ca0b 100644 --- a/src/everything/tools/toggle-simulated-logging.ts +++ b/src/everything/tools/toggle-simulated-logging.ts @@ -1,22 +1,18 @@ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; -import { - beginSimulatedLogging, - stopSimulatedLogging, -} from "../server/logging.js"; +import { McpServer, CallToolResult } from '@modelcontextprotocol/server'; +import { beginSimulatedLogging, stopSimulatedLogging } from '../server/logging.js'; // Tool configuration -const name = "toggle-simulated-logging"; +const name = 'toggle-simulated-logging'; const config = { - title: "Toggle Simulated Logging", - description: "Toggles simulated, random-leveled logging on or off.", - inputSchema: {}, - annotations: { - readOnlyHint: false, - destructiveHint: false, - idempotentHint: false, - openWorldHint: false, - }, + title: 'Toggle Simulated Logging', + description: 'Toggles simulated, random-leveled logging on or off.', + inputSchema: {}, + annotations: { + readOnlyHint: false, + destructiveHint: false, + idempotentHint: false, + openWorldHint: false + } }; // Track enabled clients by session id @@ -35,26 +31,22 @@ const clients: Set = new Set(); * @param {McpServer} server - The McpServer instance where the tool will be registered. */ export const registerToggleSimulatedLoggingTool = (server: McpServer) => { - server.registerTool( - name, - config, - async (_args, extra): Promise => { - const sessionId = extra?.sessionId; + server.registerTool(name, config, async (_args, ctx): Promise => { + const sessionId = ctx?.sessionId; - let response: string; - if (clients.has(sessionId)) { - stopSimulatedLogging(sessionId); - clients.delete(sessionId); - response = `Stopped simulated logging for session ${sessionId}`; - } else { - beginSimulatedLogging(server, sessionId); - clients.add(sessionId); - response = `Started simulated, random-leveled logging for session ${sessionId} at a 5 second pace. Client's selected logging level will be respected. If an interval elapses and the message to be sent is below the selected level, it will not be sent. Thus at higher chosen logging levels, messages should arrive further apart. `; - } + let response: string; + if (clients.has(sessionId)) { + stopSimulatedLogging(sessionId); + clients.delete(sessionId); + response = `Stopped simulated logging for session ${sessionId}`; + } else { + beginSimulatedLogging(server, sessionId); + clients.add(sessionId); + response = `Started simulated, random-leveled logging for session ${sessionId} at a 5 second pace. Client's selected logging level will be respected. If an interval elapses and the message to be sent is below the selected level, it will not be sent. Thus at higher chosen logging levels, messages should arrive further apart. `; + } - return { - content: [{ type: "text", text: `${response}` }], - }; - } - ); + return { + content: [{ type: 'text', text: `${response}` }] + }; + }); }; diff --git a/src/everything/tools/toggle-subscriber-updates.ts b/src/everything/tools/toggle-subscriber-updates.ts index 759ec20e51..68fcd7f634 100644 --- a/src/everything/tools/toggle-subscriber-updates.ts +++ b/src/everything/tools/toggle-subscriber-updates.ts @@ -1,22 +1,18 @@ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; -import { - beginSimulatedResourceUpdates, - stopSimulatedResourceUpdates, -} from "../resources/subscriptions.js"; +import { McpServer, CallToolResult } from '@modelcontextprotocol/server'; +import { beginSimulatedResourceUpdates, stopSimulatedResourceUpdates } from '../resources/subscriptions.js'; // Tool configuration -const name = "toggle-subscriber-updates"; +const name = 'toggle-subscriber-updates'; const config = { - title: "Toggle Subscriber Updates", - description: "Toggles simulated resource subscription updates on or off.", - inputSchema: {}, - annotations: { - readOnlyHint: false, - destructiveHint: false, - idempotentHint: false, - openWorldHint: false, - }, + title: 'Toggle Subscriber Updates', + description: 'Toggles simulated resource subscription updates on or off.', + inputSchema: {}, + annotations: { + readOnlyHint: false, + destructiveHint: false, + idempotentHint: false, + openWorldHint: false + } }; // Track enabled clients by session id @@ -38,26 +34,22 @@ const clients: Set = new Set(); * @param {McpServer} server - The McpServer instance where the tool will be registered. */ export const registerToggleSubscriberUpdatesTool = (server: McpServer) => { - server.registerTool( - name, - config, - async (_args, extra): Promise => { - const sessionId = extra?.sessionId; + server.registerTool(name, config, async (_args, ctx): Promise => { + const sessionId = ctx?.sessionId; - let response: string; - if (clients.has(sessionId)) { - stopSimulatedResourceUpdates(sessionId); - clients.delete(sessionId); - response = `Stopped simulated resource updates for session ${sessionId}`; - } else { - beginSimulatedResourceUpdates(server, sessionId); - clients.add(sessionId); - response = `Started simulated resource updated notifications for session ${sessionId} at a 5 second pace. Client will receive updates for any resources the it is subscribed to.`; - } + let response: string; + if (clients.has(sessionId)) { + stopSimulatedResourceUpdates(sessionId); + clients.delete(sessionId); + response = `Stopped simulated resource updates for session ${sessionId}`; + } else { + beginSimulatedResourceUpdates(server, sessionId); + clients.add(sessionId); + response = `Started simulated resource updated notifications for session ${sessionId} at a 5 second pace. Client will receive updates for any resources the it is subscribed to.`; + } - return { - content: [{ type: "text", text: `${response}` }], - }; - } - ); + return { + content: [{ type: 'text', text: `${response}` }] + }; + }); }; diff --git a/src/everything/tools/trigger-elicitation-request-async.ts b/src/everything/tools/trigger-elicitation-request-async.ts index d6e61b7bf7..8b50c1c5d6 100644 --- a/src/everything/tools/trigger-elicitation-request-async.ts +++ b/src/everything/tools/trigger-elicitation-request-async.ts @@ -1,22 +1,21 @@ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; -import { z } from "zod"; +import { McpServer, CallToolResult } from '@modelcontextprotocol/server'; +import { z } from 'zod'; // Tool configuration -const name = "trigger-elicitation-request-async"; +const name = 'trigger-elicitation-request-async'; const config = { - title: "Trigger Async Elicitation Request Tool", - description: - "Trigger an async elicitation request that the CLIENT executes as a background task. " + - "Demonstrates bidirectional MCP tasks where the server sends an elicitation request and " + - "the client handles user input asynchronously, allowing the server to poll for completion.", - inputSchema: {}, - annotations: { - readOnlyHint: false, - destructiveHint: false, - idempotentHint: false, - openWorldHint: false, - }, + title: 'Trigger Async Elicitation Request Tool', + description: + 'Trigger an async elicitation request that the CLIENT executes as a background task. ' + + 'Demonstrates bidirectional MCP tasks where the server sends an elicitation request and ' + + 'the client handles user input asynchronously, allowing the server to poll for completion.', + inputSchema: {}, + annotations: { + readOnlyHint: false, + destructiveHint: false, + idempotentHint: false, + openWorldHint: false + } }; // Poll interval in milliseconds @@ -37,233 +36,203 @@ const MAX_POLL_ATTEMPTS = 600; * * @param {McpServer} server - The McpServer instance where the tool will be registered. */ -export const registerTriggerElicitationRequestAsyncTool = ( - server: McpServer -) => { - // Check client capabilities - const clientCapabilities = server.server.getClientCapabilities() || {}; - - // Client must support elicitation AND tasks.requests.elicitation - const clientSupportsElicitation = - clientCapabilities.elicitation !== undefined; - const clientTasksCapability = clientCapabilities.tasks as - | { - requests?: { elicitation?: { create?: object } }; - } - | undefined; - const clientSupportsAsyncElicitation = - clientTasksCapability?.requests?.elicitation?.create !== undefined; +export const registerTriggerElicitationRequestAsyncTool = (server: McpServer) => { + // Check client capabilities + const clientCapabilities = server.server.getClientCapabilities() || {}; + + // Client must support elicitation AND tasks.requests.elicitation + const clientSupportsElicitation = clientCapabilities.elicitation !== undefined; + const clientTasksCapability = clientCapabilities.tasks as + | { + requests?: { elicitation?: { create?: object } }; + } + | undefined; + const clientSupportsAsyncElicitation = clientTasksCapability?.requests?.elicitation?.create !== undefined; + + if (clientSupportsElicitation && clientSupportsAsyncElicitation) { + server.registerTool(name, config, async (args, ctx): Promise => { + // Create the elicitation request WITH task metadata + // Using z.any() schema to avoid complex type matching with _meta + const request = { + method: 'elicitation/create' as const, + params: { + task: { + ttl: 600000 // 10 minutes (user input may take a while) + }, + message: 'Please provide inputs for the following fields (async task demo):', + requestedSchema: { + type: 'object' as const, + properties: { + name: { + title: 'Your Name', + type: 'string' as const, + description: 'Your full name' + }, + favoriteColor: { + title: 'Favorite Color', + type: 'string' as const, + description: 'What is your favorite color?', + enum: ['Red', 'Blue', 'Green', 'Yellow', 'Purple'] + }, + agreeToTerms: { + title: 'Terms Agreement', + type: 'boolean' as const, + description: 'Do you agree to the terms and conditions?' + } + }, + required: ['name'] + } + } + }; + + // Send the elicitation request + // Client may return either: + // - ElicitResult (synchronous execution) + // - CreateTaskResult (task-based execution with { task } object) + const elicitResponse = await ctx.mcpReq.send( + request as Parameters[0], + z.union([ + // CreateTaskResult - client created a task + z.object({ + task: z.object({ + taskId: z.string(), + status: z.string(), + pollInterval: z.number().optional(), + statusMessage: z.string().optional() + }) + }), + // ElicitResult - synchronous execution + z.object({ + action: z.string(), + content: z.any().optional() + }) + ]) + ); - if (clientSupportsElicitation && clientSupportsAsyncElicitation) { - server.registerTool( - name, - config, - async (args, extra): Promise => { - // Create the elicitation request WITH task metadata - // Using z.any() schema to avoid complex type matching with _meta - const request = { - method: "elicitation/create" as const, - params: { - task: { - ttl: 600000, // 10 minutes (user input may take a while) - }, - message: - "Please provide inputs for the following fields (async task demo):", - requestedSchema: { - type: "object" as const, - properties: { - name: { - title: "Your Name", - type: "string" as const, - description: "Your full name", + // Check if client returned CreateTaskResult (has task object) + const isTaskResult = 'task' in elicitResponse && elicitResponse.task; + if (!isTaskResult) { + // Client executed synchronously - return the direct response + return { + content: [ + { + type: 'text', + text: `[SYNC] Client executed synchronously:\n${JSON.stringify(elicitResponse, null, 2)}` + } + ] + }; + } + + const taskId = elicitResponse.task.taskId; + const statusMessages: string[] = []; + statusMessages.push(`Task created: ${taskId}`); + + // Poll for task completion + let attempts = 0; + let taskStatus = elicitResponse.task.status; + let taskStatusMessage: string | undefined; + + while (taskStatus !== 'completed' && taskStatus !== 'failed' && taskStatus !== 'cancelled' && attempts < MAX_POLL_ATTEMPTS) { + // Wait before polling + await new Promise(resolve => setTimeout(resolve, POLL_INTERVAL)); + attempts++; + + // Get task status from client + const pollResult = await ctx.mcpReq.send( + { + method: 'tasks/get', + params: { taskId } + }, + z.looseObject({ + status: z.string(), + statusMessage: z.string().optional() + }) + ); + + taskStatus = pollResult.status; + taskStatusMessage = pollResult.statusMessage; + + // Only log status changes or every 10 polls to avoid spam + if (attempts === 1 || attempts % 10 === 0 || taskStatus !== 'input_required') { + statusMessages.push(`Poll ${attempts}: ${taskStatus}${taskStatusMessage ? ` - ${taskStatusMessage}` : ''}`); + } + } + + // Check for timeout + if (attempts >= MAX_POLL_ATTEMPTS) { + return { + content: [ + { + type: 'text', + text: `[TIMEOUT] Task timed out after ${MAX_POLL_ATTEMPTS} poll attempts\n\nProgress:\n${statusMessages.join( + '\n' + )}` + } + ] + }; + } + + // Check for failure/cancellation + if (taskStatus === 'failed' || taskStatus === 'cancelled') { + return { + content: [ + { + type: 'text', + text: `[${taskStatus.toUpperCase()}] ${taskStatusMessage || 'No message'}\n\nProgress:\n${statusMessages.join( + '\n' + )}` + } + ] + }; + } + + // Fetch the final result + const result = await ctx.mcpReq.send( + { + method: 'tasks/result', + params: { taskId } }, - favoriteColor: { - title: "Favorite Color", - type: "string" as const, - description: "What is your favorite color?", - enum: ["Red", "Blue", "Green", "Yellow", "Purple"], - }, - agreeToTerms: { - title: "Terms Agreement", - type: "boolean" as const, - description: "Do you agree to the terms and conditions?", - }, - }, - required: ["name"], - }, - }, - }; - - // Send the elicitation request - // Client may return either: - // - ElicitResult (synchronous execution) - // - CreateTaskResult (task-based execution with { task } object) - const elicitResponse = await extra.sendRequest( - request as Parameters[0], - z.union([ - // CreateTaskResult - client created a task - z.object({ - task: z.object({ - taskId: z.string(), - status: z.string(), - pollInterval: z.number().optional(), - statusMessage: z.string().optional(), - }), - }), - // ElicitResult - synchronous execution - z.object({ - action: z.string(), - content: z.any().optional(), - }), - ]) - ); - - // Check if client returned CreateTaskResult (has task object) - const isTaskResult = "task" in elicitResponse && elicitResponse.task; - if (!isTaskResult) { - // Client executed synchronously - return the direct response - return { - content: [ - { - type: "text", - text: `[SYNC] Client executed synchronously:\n${JSON.stringify( - elicitResponse, - null, - 2 - )}`, - }, - ], - }; - } - - const taskId = elicitResponse.task.taskId; - const statusMessages: string[] = []; - statusMessages.push(`Task created: ${taskId}`); - - // Poll for task completion - let attempts = 0; - let taskStatus = elicitResponse.task.status; - let taskStatusMessage: string | undefined; - - while ( - taskStatus !== "completed" && - taskStatus !== "failed" && - taskStatus !== "cancelled" && - attempts < MAX_POLL_ATTEMPTS - ) { - // Wait before polling - await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL)); - attempts++; - - // Get task status from client - const pollResult = await extra.sendRequest( - { - method: "tasks/get", - params: { taskId }, - }, - z.looseObject({ - status: z.string(), - statusMessage: z.string().optional(), - }) - ); - - taskStatus = pollResult.status; - taskStatusMessage = pollResult.statusMessage; - - // Only log status changes or every 10 polls to avoid spam - if ( - attempts === 1 || - attempts % 10 === 0 || - taskStatus !== "input_required" - ) { - statusMessages.push( - `Poll ${attempts}: ${taskStatus}${ - taskStatusMessage ? ` - ${taskStatusMessage}` : "" - }` + z.any() ); - } - } - - // Check for timeout - if (attempts >= MAX_POLL_ATTEMPTS) { - return { - content: [ - { - type: "text", - text: `[TIMEOUT] Task timed out after ${MAX_POLL_ATTEMPTS} poll attempts\n\nProgress:\n${statusMessages.join( - "\n" - )}`, - }, - ], - }; - } - - // Check for failure/cancellation - if (taskStatus === "failed" || taskStatus === "cancelled") { - return { - content: [ - { - type: "text", - text: `[${taskStatus.toUpperCase()}] ${ - taskStatusMessage || "No message" - }\n\nProgress:\n${statusMessages.join("\n")}`, - }, - ], - }; - } - - // Fetch the final result - const result = await extra.sendRequest( - { - method: "tasks/result", - params: { taskId }, - }, - z.any() - ); - // Format the elicitation result - const content: CallToolResult["content"] = []; - - if (result.action === "accept" && result.content) { - content.push({ - type: "text", - text: `[COMPLETED] User provided the requested information!`, - }); - - const userData = result.content as Record; - const lines = []; - if (userData.name) lines.push(`- Name: ${userData.name}`); - if (userData.favoriteColor) - lines.push(`- Favorite Color: ${userData.favoriteColor}`); - if (userData.agreeToTerms !== undefined) - lines.push(`- Agreed to terms: ${userData.agreeToTerms}`); - - content.push({ - type: "text", - text: `User inputs:\n${lines.join("\n")}`, - }); - } else if (result.action === "decline") { - content.push({ - type: "text", - text: `[DECLINED] User declined to provide the requested information.`, - }); - } else if (result.action === "cancel") { - content.push({ - type: "text", - text: `[CANCELLED] User cancelled the elicitation dialog.`, - }); - } - - // Include progress and raw result for debugging - content.push({ - type: "text", - text: `\nProgress:\n${statusMessages.join( - "\n" - )}\n\nRaw result: ${JSON.stringify(result, null, 2)}`, + // Format the elicitation result + const content: CallToolResult['content'] = []; + + if (result.action === 'accept' && result.content) { + content.push({ + type: 'text', + text: `[COMPLETED] User provided the requested information!` + }); + + const userData = result.content as Record; + const lines = []; + if (userData.name) lines.push(`- Name: ${userData.name}`); + if (userData.favoriteColor) lines.push(`- Favorite Color: ${userData.favoriteColor}`); + if (userData.agreeToTerms !== undefined) lines.push(`- Agreed to terms: ${userData.agreeToTerms}`); + + content.push({ + type: 'text', + text: `User inputs:\n${lines.join('\n')}` + }); + } else if (result.action === 'decline') { + content.push({ + type: 'text', + text: `[DECLINED] User declined to provide the requested information.` + }); + } else if (result.action === 'cancel') { + content.push({ + type: 'text', + text: `[CANCELLED] User cancelled the elicitation dialog.` + }); + } + + // Include progress and raw result for debugging + content.push({ + type: 'text', + text: `\nProgress:\n${statusMessages.join('\n')}\n\nRaw result: ${JSON.stringify(result, null, 2)}` + }); + + return { content }; }); - - return { content }; - } - ); - } + } }; diff --git a/src/everything/tools/trigger-elicitation-request.ts b/src/everything/tools/trigger-elicitation-request.ts index ca4742141e..d3e3dae692 100644 --- a/src/everything/tools/trigger-elicitation-request.ts +++ b/src/everything/tools/trigger-elicitation-request.ts @@ -1,21 +1,18 @@ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { - ElicitResultSchema, - CallToolResult, -} from "@modelcontextprotocol/sdk/types.js"; +import { ElicitResultSchema } from '@modelcontextprotocol/core'; +import { McpServer, CallToolResult } from '@modelcontextprotocol/server'; // Tool configuration -const name = "trigger-elicitation-request"; +const name = 'trigger-elicitation-request'; const config = { - title: "Trigger Elicitation Request Tool", - description: "Trigger a Request from the Server for User Elicitation", - inputSchema: {}, - annotations: { - readOnlyHint: false, - destructiveHint: false, - idempotentHint: false, - openWorldHint: false, - }, + title: 'Trigger Elicitation Request Tool', + description: 'Trigger a Request from the Server for User Elicitation', + inputSchema: {}, + annotations: { + readOnlyHint: false, + destructiveHint: false, + idempotentHint: false, + openWorldHint: false + } }; /** @@ -37,199 +34,178 @@ const config = { * @param {McpServer} server - TThe McpServer instance where the tool will be registered. */ export const registerTriggerElicitationRequestTool = (server: McpServer) => { - // Does the client support elicitation? - const clientCapabilities = server.server.getClientCapabilities() || {}; - const clientSupportsElicitation: boolean = - clientCapabilities.elicitation !== undefined; + // 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 => { - 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: { - 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", - }, + // If so, register tool + if (clientSupportsElicitation) { + server.registerTool(name, config, async (args, ctx): Promise => { + const elicitationResult = await ctx.mcpReq.send( + { + 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: { + 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' + } + }, + 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 - ) { - content.push({ - type: "text", - text: `✅ User provided the requested information!`, - }); + 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}`); + // 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.`, - }); - } + 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: `\nRaw result: ${JSON.stringify(elicitationResult, null, 2)}`, - }); + // Include raw result for debugging + content.push({ + type: 'text', + text: `\nRaw result: ${JSON.stringify(elicitationResult, null, 2)}` + }); - return { content }; - } - ); - } + return { content }; + }); + } }; diff --git a/src/everything/tools/trigger-long-running-operation.ts b/src/everything/tools/trigger-long-running-operation.ts index 95415e88e2..54f4a98324 100644 --- a/src/everything/tools/trigger-long-running-operation.ts +++ b/src/everything/tools/trigger-long-running-operation.ts @@ -1,28 +1,24 @@ -import { z } from "zod"; -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import { z } from 'zod'; +import { McpServer, CallToolResult } from '@modelcontextprotocol/server'; // Tool input schema const TriggerLongRunningOperationSchema = z.object({ - duration: z - .number() - .default(10) - .describe("Duration of the operation in seconds"), - steps: z.number().default(5).describe("Number of steps in the operation"), + duration: z.number().default(10).describe('Duration of the operation in seconds'), + steps: z.number().default(5).describe('Number of steps in the operation') }); // Tool configuration -const name = "trigger-long-running-operation"; +const name = 'trigger-long-running-operation'; const config = { - title: "Trigger Long Running Operation Tool", - description: "Demonstrates a long running operation with progress updates.", - inputSchema: TriggerLongRunningOperationSchema, - annotations: { - readOnlyHint: true, - destructiveHint: false, - idempotentHint: true, - openWorldHint: false, - }, + title: 'Trigger Long Running Operation Tool', + description: 'Demonstrates a long running operation with progress updates.', + inputSchema: TriggerLongRunningOperationSchema, + annotations: { + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: false + } }; /** @@ -40,43 +36,37 @@ const config = { * @param {McpServer} server - The McpServer instance where the tool will be registered. */ export const registerTriggerLongRunningOperationTool = (server: McpServer) => { - server.registerTool( - name, - config, - async (args, extra): Promise => { - const validatedArgs = TriggerLongRunningOperationSchema.parse(args); - const { duration, steps } = validatedArgs; - const stepDuration = duration / steps; - const progressToken = extra._meta?.progressToken; + server.registerTool(name, config, async (args, ctx): Promise => { + const validatedArgs = TriggerLongRunningOperationSchema.parse(args); + const { duration, steps } = validatedArgs; + const stepDuration = duration / steps; + const progressToken = ctx.mcpReq._meta?.progressToken; - for (let i = 1; i < steps + 1; i++) { - await new Promise((resolve) => - setTimeout(resolve, stepDuration * 1000) - ); + for (let i = 1; i < steps + 1; i++) { + await new Promise(resolve => setTimeout(resolve, stepDuration * 1000)); - if (progressToken !== undefined) { - await server.server.notification( - { - method: "notifications/progress", - params: { - progress: i, - total: steps, - progressToken, - }, - }, - { relatedRequestId: extra.requestId } - ); + if (progressToken !== undefined) { + await server.server.notification( + { + method: 'notifications/progress', + params: { + progress: i, + total: steps, + progressToken + } + }, + { relatedRequestId: ctx.mcpReq.id } + ); + } } - } - return { - content: [ - { - type: "text", - text: `Long running operation completed. Duration: ${duration} seconds, Steps: ${steps}.`, - }, - ], - }; - } - ); + return { + content: [ + { + type: 'text', + text: `Long running operation completed. Duration: ${duration} seconds, Steps: ${steps}.` + } + ] + }; + }); }; diff --git a/src/everything/tools/trigger-sampling-request-async.ts b/src/everything/tools/trigger-sampling-request-async.ts index d61daddbd2..371eb7f379 100644 --- a/src/everything/tools/trigger-sampling-request-async.ts +++ b/src/everything/tools/trigger-sampling-request-async.ts @@ -1,34 +1,27 @@ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { - CallToolResult, - CreateMessageRequest, -} from "@modelcontextprotocol/sdk/types.js"; -import { z } from "zod"; +import { McpServer, CallToolResult, CreateMessageRequest } from '@modelcontextprotocol/server'; +import { z } from 'zod'; // Tool input schema const TriggerSamplingRequestAsyncSchema = z.object({ - prompt: z.string().describe("The prompt to send to the LLM"), - maxTokens: z - .number() - .default(100) - .describe("Maximum number of tokens to generate"), + prompt: z.string().describe('The prompt to send to the LLM'), + maxTokens: z.number().default(100).describe('Maximum number of tokens to generate') }); // Tool configuration -const name = "trigger-sampling-request-async"; +const name = 'trigger-sampling-request-async'; const config = { - title: "Trigger Async Sampling Request Tool", - description: - "Trigger an async sampling request that the CLIENT executes as a background task. " + - "Demonstrates bidirectional MCP tasks where the server sends a request and the client " + - "executes it asynchronously, allowing the server to poll for progress and results.", - inputSchema: TriggerSamplingRequestAsyncSchema, - annotations: { - readOnlyHint: false, - destructiveHint: false, - idempotentHint: false, - openWorldHint: true, - }, + title: 'Trigger Async Sampling Request Tool', + description: + 'Trigger an async sampling request that the CLIENT executes as a background task. ' + + 'Demonstrates bidirectional MCP tasks where the server sends a request and the client ' + + 'executes it asynchronously, allowing the server to poll for progress and results.', + inputSchema: TriggerSamplingRequestAsyncSchema, + annotations: { + readOnlyHint: false, + destructiveHint: false, + idempotentHint: false, + openWorldHint: true + } }; // Poll interval in milliseconds @@ -49,186 +42,167 @@ const MAX_POLL_ATTEMPTS = 60; * @param {McpServer} server - The McpServer instance where the tool will be registered. */ export const registerTriggerSamplingRequestAsyncTool = (server: McpServer) => { - // Check client capabilities - const clientCapabilities = server.server.getClientCapabilities() || {}; - - // Client must support sampling AND tasks.requests.sampling - const clientSupportsSampling = clientCapabilities.sampling !== undefined; - const clientTasksCapability = clientCapabilities.tasks as - | { - requests?: { sampling?: { createMessage?: object } }; - } - | undefined; - const clientSupportsAsyncSampling = - clientTasksCapability?.requests?.sampling?.createMessage !== undefined; - - if (clientSupportsSampling && clientSupportsAsyncSampling) { - server.registerTool( - name, - config, - async (args, extra): Promise => { - const validatedArgs = TriggerSamplingRequestAsyncSchema.parse(args); - const { prompt, maxTokens } = validatedArgs; - - // Create the sampling request WITH task metadata - // The params.task field signals to the client that this should be executed as a task - const request: CreateMessageRequest & { - params: { task?: { ttl: number } }; - } = { - method: "sampling/createMessage", - params: { - task: { - ttl: 300000, // 5 minutes - }, - messages: [ - { - role: "user", - content: { - type: "text", - text: `Resource ${name} context: ${prompt}`, + // Check client capabilities + const clientCapabilities = server.server.getClientCapabilities() || {}; + + // Client must support sampling AND tasks.requests.sampling + const clientSupportsSampling = clientCapabilities.sampling !== undefined; + const clientTasksCapability = clientCapabilities.tasks as + | { + requests?: { sampling?: { createMessage?: object } }; + } + | undefined; + const clientSupportsAsyncSampling = clientTasksCapability?.requests?.sampling?.createMessage !== undefined; + + if (clientSupportsSampling && clientSupportsAsyncSampling) { + server.registerTool(name, config, async (args, ctx): Promise => { + const validatedArgs = TriggerSamplingRequestAsyncSchema.parse(args); + const { prompt, maxTokens } = validatedArgs; + + // Create the sampling request WITH task metadata + // The params.task field signals to the client that this should be executed as a task + const request: CreateMessageRequest & { + params: { task?: { ttl: number } }; + } = { + method: 'sampling/createMessage', + params: { + task: { + ttl: 300000 // 5 minutes + }, + messages: [ + { + role: 'user', + content: { + type: 'text', + text: `Resource ${name} context: ${prompt}` + } + } + ], + systemPrompt: 'You are a helpful test server.', + maxTokens, + temperature: 0.7 + } + }; + + // Send the sampling request + // Client may return either: + // - CreateMessageResult (synchronous execution) + // - CreateTaskResult (task-based execution with { task } object) + const samplingResponse = await ctx.mcpReq.send( + request, + z.union([ + // CreateTaskResult - client created a task + z.object({ + task: z.object({ + taskId: z.string(), + status: z.string(), + pollInterval: z.number().optional(), + statusMessage: z.string().optional() + }) + }), + // CreateMessageResult - synchronous execution + z.object({ + role: z.string(), + content: z.any(), + model: z.string(), + stopReason: z.string().optional() + }) + ]) + ); + + // Check if client returned CreateTaskResult (has task object) + const isTaskResult = 'task' in samplingResponse && samplingResponse.task; + if (!isTaskResult) { + // Client executed synchronously - return the direct response + return { + content: [ + { + type: 'text', + text: `[SYNC] Client executed synchronously:\n${JSON.stringify(samplingResponse, null, 2)}` + } + ] + }; + } + + const taskId = samplingResponse.task.taskId; + const statusMessages: string[] = []; + statusMessages.push(`Task created: ${taskId}`); + + // Poll for task completion + let attempts = 0; + let taskStatus = samplingResponse.task.status; + let taskStatusMessage: string | undefined; + + while (taskStatus !== 'completed' && taskStatus !== 'failed' && taskStatus !== 'cancelled' && attempts < MAX_POLL_ATTEMPTS) { + // Wait before polling + await new Promise(resolve => setTimeout(resolve, POLL_INTERVAL)); + attempts++; + + // Get task status from client + const pollResult = await ctx.mcpReq.send( + { + method: 'tasks/get', + params: { taskId } + }, + z.looseObject({ + status: z.string(), + statusMessage: z.string().optional() + }) + ); + + taskStatus = pollResult.status; + taskStatusMessage = pollResult.statusMessage; + statusMessages.push(`Poll ${attempts}: ${taskStatus}${taskStatusMessage ? ` - ${taskStatusMessage}` : ''}`); + } + + // Check for timeout + if (attempts >= MAX_POLL_ATTEMPTS) { + return { + content: [ + { + type: 'text', + text: `[TIMEOUT] Task timed out after ${MAX_POLL_ATTEMPTS} poll attempts\n\nProgress:\n${statusMessages.join( + '\n' + )}` + } + ] + }; + } + + // Check for failure/cancellation + if (taskStatus === 'failed' || taskStatus === 'cancelled') { + return { + content: [ + { + type: 'text', + text: `[${taskStatus.toUpperCase()}] ${taskStatusMessage || 'No message'}\n\nProgress:\n${statusMessages.join( + '\n' + )}` + } + ] + }; + } + + // Fetch the final result + const result = await ctx.mcpReq.send( + { + method: 'tasks/result', + params: { taskId } }, - }, - ], - systemPrompt: "You are a helpful test server.", - maxTokens, - temperature: 0.7, - }, - }; - - // Send the sampling request - // Client may return either: - // - CreateMessageResult (synchronous execution) - // - CreateTaskResult (task-based execution with { task } object) - const samplingResponse = await extra.sendRequest( - request, - z.union([ - // CreateTaskResult - client created a task - z.object({ - task: z.object({ - taskId: z.string(), - status: z.string(), - pollInterval: z.number().optional(), - statusMessage: z.string().optional(), - }), - }), - // CreateMessageResult - synchronous execution - z.object({ - role: z.string(), - content: z.any(), - model: z.string(), - stopReason: z.string().optional(), - }), - ]) - ); - - // Check if client returned CreateTaskResult (has task object) - const isTaskResult = - "task" in samplingResponse && samplingResponse.task; - if (!isTaskResult) { - // Client executed synchronously - return the direct response - return { - content: [ - { - type: "text", - text: `[SYNC] Client executed synchronously:\n${JSON.stringify( - samplingResponse, - null, - 2 - )}`, - }, - ], - }; - } - - const taskId = samplingResponse.task.taskId; - const statusMessages: string[] = []; - statusMessages.push(`Task created: ${taskId}`); - - // Poll for task completion - let attempts = 0; - let taskStatus = samplingResponse.task.status; - let taskStatusMessage: string | undefined; - - while ( - taskStatus !== "completed" && - taskStatus !== "failed" && - taskStatus !== "cancelled" && - attempts < MAX_POLL_ATTEMPTS - ) { - // Wait before polling - await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL)); - attempts++; - - // Get task status from client - const pollResult = await extra.sendRequest( - { - method: "tasks/get", - params: { taskId }, - }, - z.looseObject({ - status: z.string(), - statusMessage: z.string().optional(), - }) - ); - - taskStatus = pollResult.status; - taskStatusMessage = pollResult.statusMessage; - statusMessages.push( - `Poll ${attempts}: ${taskStatus}${ - taskStatusMessage ? ` - ${taskStatusMessage}` : "" - }` - ); - } - - // Check for timeout - if (attempts >= MAX_POLL_ATTEMPTS) { - return { - content: [ - { - type: "text", - text: `[TIMEOUT] Task timed out after ${MAX_POLL_ATTEMPTS} poll attempts\n\nProgress:\n${statusMessages.join( - "\n" - )}`, - }, - ], - }; - } - - // Check for failure/cancellation - if (taskStatus === "failed" || taskStatus === "cancelled") { - return { - content: [ - { - type: "text", - text: `[${taskStatus.toUpperCase()}] ${ - taskStatusMessage || "No message" - }\n\nProgress:\n${statusMessages.join("\n")}`, - }, - ], - }; - } - - // Fetch the final result - const result = await extra.sendRequest( - { - method: "tasks/result", - params: { taskId }, - }, - z.any() - ); - - // Return the result with status history - return { - content: [ - { - type: "text", - text: `[COMPLETED] Async sampling completed!\n\n**Progress:**\n${statusMessages.join( - "\n" - )}\n\n**Result:**\n${JSON.stringify(result, null, 2)}`, - }, - ], - }; - } - ); - } + z.any() + ); + + // Return the result with status history + return { + content: [ + { + type: 'text', + text: `[COMPLETED] Async sampling completed!\n\n**Progress:**\n${statusMessages.join( + '\n' + )}\n\n**Result:**\n${JSON.stringify(result, null, 2)}` + } + ] + }; + }); + } }; diff --git a/src/everything/tools/trigger-sampling-request.ts b/src/everything/tools/trigger-sampling-request.ts index fd9b5375c4..54ab0f6a07 100644 --- a/src/everything/tools/trigger-sampling-request.ts +++ b/src/everything/tools/trigger-sampling-request.ts @@ -1,32 +1,25 @@ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { - CallToolResult, - CreateMessageRequest, - CreateMessageResultSchema, -} from "@modelcontextprotocol/sdk/types.js"; -import { z } from "zod"; +import { CreateMessageResultSchema } from '@modelcontextprotocol/core'; +import { McpServer, CallToolResult, CreateMessageRequest } from '@modelcontextprotocol/server'; +import { z } from 'zod'; // Tool input schema const TriggerSamplingRequestSchema = z.object({ - prompt: z.string().describe("The prompt to send to the LLM"), - maxTokens: z - .number() - .default(100) - .describe("Maximum number of tokens to generate"), + prompt: z.string().describe('The prompt to send to the LLM'), + maxTokens: z.number().default(100).describe('Maximum number of tokens to generate') }); // Tool configuration -const name = "trigger-sampling-request"; +const name = 'trigger-sampling-request'; const config = { - title: "Trigger Sampling Request Tool", - description: "Trigger a Request from the Server for LLM Sampling", - inputSchema: TriggerSamplingRequestSchema, - annotations: { - readOnlyHint: false, - destructiveHint: false, - idempotentHint: false, - openWorldHint: true, - }, + title: 'Trigger Sampling Request Tool', + description: 'Trigger a Request from the Server for LLM Sampling', + inputSchema: TriggerSamplingRequestSchema, + annotations: { + readOnlyHint: false, + destructiveHint: false, + idempotentHint: false, + openWorldHint: true + } }; /** @@ -43,55 +36,47 @@ const config = { * @param {McpServer} server - The McpServer instance where the tool will be registered. */ export const registerTriggerSamplingRequestTool = (server: McpServer) => { - // Does the client support sampling? - const clientCapabilities = server.server.getClientCapabilities() || {}; - const clientSupportsSampling: boolean = - clientCapabilities.sampling !== undefined; + // Does the client support sampling? + const clientCapabilities = server.server.getClientCapabilities() || {}; + const clientSupportsSampling: boolean = clientCapabilities.sampling !== undefined; - // If so, register tool - if (clientSupportsSampling) { - server.registerTool( - name, - config, - async (args, extra): Promise => { - const validatedArgs = TriggerSamplingRequestSchema.parse(args); - const { prompt, maxTokens } = validatedArgs; + // If so, register tool + if (clientSupportsSampling) { + server.registerTool(name, config, async (args, ctx): Promise => { + const validatedArgs = TriggerSamplingRequestSchema.parse(args); + const { prompt, maxTokens } = validatedArgs; - // Create the sampling request - const request: CreateMessageRequest = { - method: "sampling/createMessage", - params: { - messages: [ - { - role: "user", - content: { - type: "text", - text: `Resource ${name} context: ${prompt}`, - }, - }, - ], - systemPrompt: "You are a helpful test server.", - maxTokens, - temperature: 0.7, - }, - }; + // Create the sampling request + const request: CreateMessageRequest = { + method: 'sampling/createMessage', + params: { + messages: [ + { + role: 'user', + content: { + type: 'text', + text: `Resource ${name} context: ${prompt}` + } + } + ], + systemPrompt: 'You are a helpful test server.', + maxTokens, + temperature: 0.7 + } + }; - // Send the sampling request to the client - const result = await extra.sendRequest( - request, - CreateMessageResultSchema - ); + // Send the sampling request to the client + const result = await ctx.mcpReq.send(request, CreateMessageResultSchema); - // Return the result to the client - return { - content: [ - { - type: "text", - text: `LLM sampling result: \n${JSON.stringify(result, null, 2)}`, - }, - ], - }; - } - ); - } + // Return the result to the client + return { + content: [ + { + type: 'text', + text: `LLM sampling result: \n${JSON.stringify(result, null, 2)}` + } + ] + }; + }); + } }; diff --git a/src/everything/tools/trigger-url-elicitation.ts b/src/everything/tools/trigger-url-elicitation.ts index a7de878de7..91e1015d34 100644 --- a/src/everything/tools/trigger-url-elicitation.ts +++ b/src/everything/tools/trigger-url-elicitation.ts @@ -1,53 +1,45 @@ -import { randomUUID } from "node:crypto"; -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { - CallToolResult, - ElicitRequestURLParams, - ElicitResultSchema, - UrlElicitationRequiredError, -} from "@modelcontextprotocol/sdk/types.js"; -import { z } from "zod"; +import { randomUUID } from 'node:crypto'; +import { ElicitResultSchema } from '@modelcontextprotocol/core'; +import { McpServer, CallToolResult, ElicitRequestURLParams, UrlElicitationRequiredError } from '@modelcontextprotocol/server'; +import { z } from 'zod'; // Tool input schema const TriggerUrlElicitationSchema = z.object({ - url: z.string().url().describe("The URL the user should open"), - message: z - .string() - .default("Please open the link to complete this action.") - .describe("Message shown to the user before opening the URL"), - elicitationId: z - .string() - .optional() - .describe("Optional explicit elicitation ID. Defaults to a random UUID."), - errorPath: z - .boolean() - .default(false) - .describe( - "Controls which elicitation mechanism is used. " + - "When false (default), sends an elicitation/create request (request path). " + - "When true, throws a UrlElicitationRequiredError (MCP error code -32042) so the client handles " + - "the URL elicitation via the error path rather than waiting for a response. " + - "To clear the error, satisfy the prerequisite and retry this call with the same arguments; the " + - "retry ignores errorPath and proceeds, so the client does not loop on the same error." - ), + url: z.string().url().describe('The URL the user should open'), + message: z + .string() + .default('Please open the link to complete this action.') + .describe('Message shown to the user before opening the URL'), + elicitationId: z.string().optional().describe('Optional explicit elicitation ID. Defaults to a random UUID.'), + errorPath: z + .boolean() + .default(false) + .describe( + 'Controls which elicitation mechanism is used. ' + + 'When false (default), sends an elicitation/create request (request path). ' + + 'When true, throws a UrlElicitationRequiredError (MCP error code -32042) so the client handles ' + + 'the URL elicitation via the error path rather than waiting for a response. ' + + 'To clear the error, satisfy the prerequisite and retry this call with the same arguments; the ' + + 'retry ignores errorPath and proceeds, so the client does not loop on the same error.' + ) }); // Tool configuration -const name = "trigger-url-elicitation"; +const name = 'trigger-url-elicitation'; const config = { - title: "Trigger URL Elicitation Tool", - description: - "Trigger a URL elicitation so the client can direct the user to a browser flow. " + - "Supports two mechanisms: the request path (elicitation/create, default) which awaits the user's " + - "response, and the error path (UrlElicitationRequiredError, -32042) which signals the client " + - "to handle URL elicitation via the error response. Set errorPath=true to use the error path.", - inputSchema: TriggerUrlElicitationSchema, - annotations: { - readOnlyHint: false, - destructiveHint: false, - idempotentHint: false, - openWorldHint: true, - }, + title: 'Trigger URL Elicitation Tool', + description: + 'Trigger a URL elicitation so the client can direct the user to a browser flow. ' + + "Supports two mechanisms: the request path (elicitation/create, default) which awaits the user's " + + 'response, and the error path (UrlElicitationRequiredError, -32042) which signals the client ' + + 'to handle URL elicitation via the error response. Set errorPath=true to use the error path.', + inputSchema: TriggerUrlElicitationSchema, + annotations: { + readOnlyHint: false, + destructiveHint: false, + idempotentHint: false, + openWorldHint: true + } }; /** @@ -73,8 +65,7 @@ const issuedErrorPathElicitations = new Set(); * Test-only helper to reset the module-level error-path state between cases. * Not part of the tool's public behavior. */ -export const __resetIssuedErrorPathElicitations = () => - issuedErrorPathElicitations.clear(); +export const __resetIssuedErrorPathElicitations = () => issuedErrorPathElicitations.clear(); /** * Registers the 'trigger-url-elicitation' tool. @@ -93,123 +84,106 @@ export const __resetIssuedErrorPathElicitations = () => * @param {McpServer} server - The McpServer instance where the tool will be registered. */ export const registerTriggerUrlElicitationTool = (server: McpServer) => { - const clientCapabilities = server.server.getClientCapabilities() || {}; - const clientElicitationCapabilities = clientCapabilities.elicitation as - | { - url?: object; - } - | undefined; - - const clientSupportsUrlElicitation = - clientElicitationCapabilities?.url !== undefined; - - if (clientSupportsUrlElicitation) { - server.registerTool( - name, - config, - async (args, extra): Promise => { - const { - url, - message, - elicitationId: requestedElicitationId, - errorPath, - } = args; - - const elicitationId = requestedElicitationId ?? randomUUID(); - const sessionId = extra.sessionId ?? "default"; - - // Key the one-shot error-path marker on inputs the client resends - // verbatim when it retries the original tool call. A real client retries - // with the *same* arguments and does NOT echo the prerequisite's - // (server-generated) elicitationId, so we must key on stable inputs: - // the session, the requested URL, and the caller-supplied elicitationId - // (if any). Keying on the resolved/random elicitationId would change on - // every call and never match, re-throwing the prerequisite forever. - const errorPathKey = `${sessionId}\u0000${url}\u0000${requestedElicitationId ?? ""}`; - - const elicitationParams: ElicitRequestURLParams = { - mode: "url", - url, - message, - elicitationId, - }; - - // Error path: signal the client via UrlElicitationRequiredError (-32042) - // so it handles a prerequisite URL elicitation before this request can - // proceed. Two things keep the client from looping forever: - // - // 1. The prerequisite points at a *different* URL than the one that - // failed. Reusing the original `url` would make the client complete - // the prerequisite, retry, and hit the same -32042 error endlessly. - // 2. We remember that we issued a prerequisite for this request. When - // the client satisfies it and retries the same call, we recognize - // the retry, *ignore* errorPath, and fall through to the request - // path. Without this, the retry would re-enter the error path and - // re-request the prerequisite URL — another loop. - if (errorPath) { - if (issuedErrorPathElicitations.has(errorPathKey)) { - // Retry of a satisfied prerequisite: clear the one-shot marker and - // ignore errorPath, falling through to the request path below. - issuedErrorPathElicitations.delete(errorPathKey); - } else { - // Originating call: record that we issued a prerequisite for this - // request, then signal the client via -32042. - issuedErrorPathElicitations.add(errorPathKey); - const prerequisiteElicitation: ElicitRequestURLParams = { - mode: "url", - url: "https://modelcontextprotocol.io", - message: - "Open this link to satisfy the prerequisite, then retry the request.", - elicitationId: randomUUID(), + const clientCapabilities = server.server.getClientCapabilities() || {}; + const clientElicitationCapabilities = clientCapabilities.elicitation as + | { + url?: object; + } + | undefined; + + const clientSupportsUrlElicitation = clientElicitationCapabilities?.url !== undefined; + + if (clientSupportsUrlElicitation) { + server.registerTool(name, config, async (args, ctx): Promise => { + const { url, message, elicitationId: requestedElicitationId, errorPath } = args; + + const elicitationId = requestedElicitationId ?? randomUUID(); + const sessionId = ctx.sessionId ?? 'default'; + + // Key the one-shot error-path marker on inputs the client resends + // verbatim when it retries the original tool call. A real client retries + // with the *same* arguments and does NOT echo the prerequisite's + // (server-generated) elicitationId, so we must key on stable inputs: + // the session, the requested URL, and the caller-supplied elicitationId + // (if any). Keying on the resolved/random elicitationId would change on + // every call and never match, re-throwing the prerequisite forever. + const errorPathKey = `${sessionId}\u0000${url}\u0000${requestedElicitationId ?? ''}`; + + const elicitationParams: ElicitRequestURLParams = { + mode: 'url', + url, + message, + elicitationId }; - throw new UrlElicitationRequiredError( - [prerequisiteElicitation], - "This request requires browser-based authorization." + + // Error path: signal the client via UrlElicitationRequiredError (-32042) + // so it handles a prerequisite URL elicitation before this request can + // proceed. Two things keep the client from looping forever: + // + // 1. The prerequisite points at a *different* URL than the one that + // failed. Reusing the original `url` would make the client complete + // the prerequisite, retry, and hit the same -32042 error endlessly. + // 2. We remember that we issued a prerequisite for this request. When + // the client satisfies it and retries the same call, we recognize + // the retry, *ignore* errorPath, and fall through to the request + // path. Without this, the retry would re-enter the error path and + // re-request the prerequisite URL — another loop. + if (errorPath) { + if (issuedErrorPathElicitations.has(errorPathKey)) { + // Retry of a satisfied prerequisite: clear the one-shot marker and + // ignore errorPath, falling through to the request path below. + issuedErrorPathElicitations.delete(errorPathKey); + } else { + // Originating call: record that we issued a prerequisite for this + // request, then signal the client via -32042. + issuedErrorPathElicitations.add(errorPathKey); + const prerequisiteElicitation: ElicitRequestURLParams = { + mode: 'url', + url: 'https://modelcontextprotocol.io', + message: 'Open this link to satisfy the prerequisite, then retry the request.', + elicitationId: randomUUID() + }; + throw new UrlElicitationRequiredError([prerequisiteElicitation], 'This request requires browser-based authorization.'); + } + } + + // Request path: send elicitation/create and await the user's response + const elicitationResult = await ctx.mcpReq.send( + { + method: 'elicitation/create', + params: elicitationParams + }, + ElicitResultSchema, + { timeout: 10 * 60 * 1000 /* 10 minutes */ } ); - } - } - - // Request path: send elicitation/create and await the user's response - const elicitationResult = await extra.sendRequest( - { - method: "elicitation/create", - params: elicitationParams, - }, - ElicitResultSchema, - { timeout: 10 * 60 * 1000 /* 10 minutes */ } - ); - - // Handle different response actions - const content: CallToolResult["content"] = []; - - if (elicitationResult.action === "accept") { - content.push({ - type: "text", - text: - `✅ User completed the URL elicitation flow.\n` + - `Elicitation ID: ${elicitationId}\n` + - `URL: ${url}`, - }); - } else if (elicitationResult.action === "decline") { - content.push({ - type: "text", - text: `❌ User declined to open the URL (Elicitation ID: ${elicitationId}).`, - }); - } else if (elicitationResult.action === "cancel") { - content.push({ - type: "text", - text: `⚠️ User cancelled the URL elicitation (Elicitation ID: ${elicitationId}).`, - }); - } - - // Include raw result for debugging - content.push({ - type: "text", - text: `\nRaw result: ${JSON.stringify(elicitationResult, null, 2)}`, - }); - return { content }; - } - ); - } + // Handle different response actions + const content: CallToolResult['content'] = []; + + if (elicitationResult.action === 'accept') { + content.push({ + type: 'text', + text: `✅ User completed the URL elicitation flow.\n` + `Elicitation ID: ${elicitationId}\n` + `URL: ${url}` + }); + } else if (elicitationResult.action === 'decline') { + content.push({ + type: 'text', + text: `❌ User declined to open the URL (Elicitation ID: ${elicitationId}).` + }); + } else if (elicitationResult.action === 'cancel') { + content.push({ + type: 'text', + text: `⚠️ User cancelled the URL elicitation (Elicitation ID: ${elicitationId}).` + }); + } + + // Include raw result for debugging + content.push({ + type: 'text', + text: `\nRaw result: ${JSON.stringify(elicitationResult, null, 2)}` + }); + + return { content }; + }); + } }; diff --git a/src/everything/transports/sse.ts b/src/everything/transports/sse.ts index 2406db7cfa..1c4f096b9e 100644 --- a/src/everything/transports/sse.ts +++ b/src/everything/transports/sse.ts @@ -1,77 +1,74 @@ -import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; -import express from "express"; -import { createServer } from "../server/index.js"; -import cors from "cors"; +import { SSEServerTransport } from '@modelcontextprotocol/server-legacy/sse'; +import express from 'express'; +import { createServer } from '../server/index.js'; +import cors from 'cors'; -console.error("Starting SSE server..."); +console.error('Starting SSE server...'); // Express app with permissive CORS for testing with Inspector direct connect mode const app = express(); app.use( - cors({ - origin: "*", // use "*" with caution in production - methods: "GET,POST", - preflightContinue: false, - optionsSuccessStatus: 204, - }) + cors({ + origin: '*', // use "*" with caution in production + methods: 'GET,POST', + preflightContinue: false, + optionsSuccessStatus: 204 + }) ); // Map sessionId to transport for each client -const transports: Map = new Map< - string, - SSEServerTransport ->(); +const transports: Map = new Map(); // Handle GET requests for new SSE streams -app.get("/sse", async (req, res) => { - let transport: SSEServerTransport; - const { server, cleanup } = createServer(); +app.get('/sse', async (req, res) => { + let transport: SSEServerTransport; + const { server, cleanup } = createServer(); - // Session Id should not exist for GET /sse requests - if (req?.query?.sessionId) { - const sessionId = req?.query?.sessionId as string; - transport = transports.get(sessionId) as SSEServerTransport; - console.error( - "Client Reconnecting? This shouldn't happen; when client has a sessionId, GET /sse should not be called again.", - transport.sessionId - ); - } else { - // Create and store transport for the new session - transport = new SSEServerTransport("/message", res); - transports.set(transport.sessionId, transport); + // Session Id should not exist for GET /sse requests + if (req?.query?.sessionId) { + const sessionId = req?.query?.sessionId as string; + transport = transports.get(sessionId) as SSEServerTransport; + console.error( + "Client Reconnecting? This shouldn't happen; when client has a sessionId, GET /sse should not be called again.", + transport.sessionId + ); + } else { + // Create and store transport for the new session + transport = new SSEServerTransport('/message', res); + transports.set(transport.sessionId, transport); - // Connect server to transport - await server.connect(transport); - const sessionId = transport.sessionId; - console.error("Client Connected: ", sessionId); + // Connect server to transport + await server.connect(transport); + const sessionId = transport.sessionId; + console.error('Client Connected: ', sessionId); - // Handle close of connection - server.server.onclose = async () => { - const sessionId = transport.sessionId; - console.error("Client Disconnected: ", sessionId); - transports.delete(sessionId); - cleanup(sessionId); - }; - } + // Handle close of connection + server.server.onclose = async () => { + const sessionId = transport.sessionId; + console.error('Client Disconnected: ', sessionId); + transports.delete(sessionId); + cleanup(sessionId); + }; + } }); // Handle POST requests for client messages -app.post("/message", async (req, res) => { - // Session Id should exist for POST /message requests - const sessionId = req?.query?.sessionId as string; +app.post('/message', async (req, res) => { + // Session Id should exist for POST /message requests + const sessionId = req?.query?.sessionId as string; - // Get the transport for this session and use it to handle the request - const transport = transports.get(sessionId); - if (transport) { - console.error("Client Message from", sessionId); - await transport.handlePostMessage(req, res); - } else { - console.error(`No transport found for sessionId ${sessionId}`); - } + // Get the transport for this session and use it to handle the request + const transport = transports.get(sessionId); + if (transport) { + console.error('Client Message from', sessionId); + await transport.handlePostMessage(req, res); + } else { + console.error(`No transport found for sessionId ${sessionId}`); + } }); // Start the express server const PORT = process.env.PORT || 3001; app.listen(PORT, () => { - console.error(`Server is running on port ${PORT}`); + console.error(`Server is running on port ${PORT}`); }); diff --git a/src/everything/transports/stdio.ts b/src/everything/transports/stdio.ts index 3e653bcf4d..a72d3bad2d 100644 --- a/src/everything/transports/stdio.ts +++ b/src/everything/transports/stdio.ts @@ -1,9 +1,9 @@ #!/usr/bin/env node -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { createServer } from "../server/index.js"; +import { StdioServerTransport } from '@modelcontextprotocol/server/stdio'; +import { createServer } from '../server/index.js'; -console.error("Starting default (STDIO) server..."); +console.error('Starting default (STDIO) server...'); /** * The main method @@ -13,21 +13,21 @@ console.error("Starting default (STDIO) server..."); * @return {Promise} A promise that resolves when the main function has executed and the process exits. */ async function main(): Promise { - const transport = new StdioServerTransport(); - const { server, cleanup } = createServer(); + const transport = new StdioServerTransport(); + const { server, cleanup } = createServer(); - // Connect transport to server - await server.connect(transport); + // Connect transport to server + await server.connect(transport); - // Cleanup on exit - process.on("SIGINT", async () => { - await server.close(); - cleanup(); - process.exit(0); - }); + // Cleanup on exit + process.on('SIGINT', async () => { + await server.close(); + cleanup(); + process.exit(0); + }); } -main().catch((error) => { - console.error("Server error:", error); - process.exit(1); +main().catch(error => { + console.error('Server error:', error); + process.exit(1); }); diff --git a/src/everything/transports/streamableHttp.ts b/src/everything/transports/streamableHttp.ts index 2e79abc554..251d27e9e3 100644 --- a/src/everything/transports/streamableHttp.ts +++ b/src/everything/transports/streamableHttp.ts @@ -1,240 +1,227 @@ -import { - StreamableHTTPServerTransport, - EventStore, -} from "@modelcontextprotocol/sdk/server/streamableHttp.js"; -import express, { Request, Response } from "express"; -import { createServer } from "../server/index.js"; -import { randomUUID } from "node:crypto"; -import cors from "cors"; +import { EventStore } from '@modelcontextprotocol/server'; +import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node'; +import express, { Request, Response } from 'express'; +import { createServer } from '../server/index.js'; +import { randomUUID } from 'node:crypto'; +import cors from 'cors'; // Simple in-memory event store for SSE resumability class InMemoryEventStore implements EventStore { - private events: Map = - new Map(); - - async storeEvent(streamId: string, message: unknown): Promise { - const eventId = randomUUID(); - this.events.set(eventId, { streamId, message }); - return eventId; - } - - async replayEventsAfter( - lastEventId: string, - { send }: { send: (eventId: string, message: unknown) => Promise } - ): Promise { - const entries = Array.from(this.events.entries()); - const startIndex = entries.findIndex(([id]) => id === lastEventId); - if (startIndex === -1) return lastEventId; - - let lastId: string = lastEventId; - for (let i = startIndex + 1; i < entries.length; i++) { - const [eventId, { message }] = entries[i]; - await send(eventId, message); - lastId = eventId; + private events: Map = new Map(); + + async storeEvent(streamId: string, message: unknown): Promise { + const eventId = randomUUID(); + this.events.set(eventId, { streamId, message }); + return eventId; + } + + async replayEventsAfter( + lastEventId: string, + { send }: { send: (eventId: string, message: unknown) => Promise } + ): Promise { + const entries = Array.from(this.events.entries()); + const startIndex = entries.findIndex(([id]) => id === lastEventId); + if (startIndex === -1) return lastEventId; + + let lastId: string = lastEventId; + for (let i = startIndex + 1; i < entries.length; i++) { + const [eventId, { message }] = entries[i]; + await send(eventId, message); + lastId = eventId; + } + return lastId; } - return lastId; - } } -console.log("Starting Streamable HTTP server..."); +console.log('Starting Streamable HTTP server...'); // Express app with permissive CORS for testing with Inspector direct connect mode const app = express(); app.use( - cors({ - origin: "*", // use "*" with caution in production - methods: "GET,POST,DELETE", - preflightContinue: false, - optionsSuccessStatus: 204, - exposedHeaders: ["mcp-session-id", "last-event-id", "mcp-protocol-version"], - }) + cors({ + origin: '*', // use "*" with caution in production + methods: 'GET,POST,DELETE', + preflightContinue: false, + optionsSuccessStatus: 204, + exposedHeaders: ['mcp-session-id', 'last-event-id', 'mcp-protocol-version'] + }) ); // Map sessionId to server transport for each client -const transports: Map = new Map< - string, - StreamableHTTPServerTransport ->(); +const transports: Map = new Map(); // Handle POST requests for client messages -app.post("/mcp", async (req: Request, res: Response) => { - console.log("Received MCP POST request"); - try { - // Check for existing session ID - const sessionId = req.headers["mcp-session-id"] as string | undefined; - - let transport: StreamableHTTPServerTransport; - - if (sessionId && transports.has(sessionId)) { - // Reuse existing transport - transport = transports.get(sessionId)!; - } else if (!sessionId) { - const { server, cleanup } = createServer(); - - // New initialization request - const eventStore = new InMemoryEventStore(); - transport = new StreamableHTTPServerTransport({ - sessionIdGenerator: () => randomUUID(), - eventStore, // Enable resumability - onsessioninitialized: (sessionId: string) => { - // Store the transport by session ID when a session is initialized - // This avoids race conditions where requests might come in before the session is stored - console.log(`Session initialized with ID: ${sessionId}`); - transports.set(sessionId, transport); - }, - }); - - // Set up onclose handler to clean up transport when closed - server.server.onclose = async () => { - const sid = transport.sessionId; - if (sid && transports.has(sid)) { - console.log( - `Transport closed for session ${sid}, removing from transports map` - ); - transports.delete(sid); - cleanup(sid); +app.post('/mcp', async (req: Request, res: Response) => { + console.log('Received MCP POST request'); + try { + // Check for existing session ID + const sessionId = req.headers['mcp-session-id'] as string | undefined; + + let transport: NodeStreamableHTTPServerTransport; + + if (sessionId && transports.has(sessionId)) { + // Reuse existing transport + transport = transports.get(sessionId)!; + } else if (!sessionId) { + const { server, cleanup } = createServer(); + + // New initialization request + const eventStore = new InMemoryEventStore(); + transport = new NodeStreamableHTTPServerTransport({ + sessionIdGenerator: () => randomUUID(), + eventStore, // Enable resumability + onsessioninitialized: (sessionId: string) => { + // Store the transport by session ID when a session is initialized + // This avoids race conditions where requests might come in before the session is stored + console.log(`Session initialized with ID: ${sessionId}`); + transports.set(sessionId, transport); + } + }); + + // Set up onclose handler to clean up transport when closed + server.server.onclose = async () => { + const sid = transport.sessionId; + if (sid && transports.has(sid)) { + console.log(`Transport closed for session ${sid}, removing from transports map`); + transports.delete(sid); + cleanup(sid); + } + }; + + // Connect the transport to the MCP server BEFORE handling the request + // so responses can flow back through the same transport + await server.connect(transport); + await transport.handleRequest(req, res); + return; + } else { + // Invalid request - no session ID or not initialization request + res.status(400).json({ + jsonrpc: '2.0', + error: { + code: -32000, + message: 'Bad Request: No valid session ID provided' + }, + id: req?.body?.id + }); + return; } - }; - - // Connect the transport to the MCP server BEFORE handling the request - // so responses can flow back through the same transport - await server.connect(transport); - await transport.handleRequest(req, res); - return; - } else { - // Invalid request - no session ID or not initialization request - res.status(400).json({ - jsonrpc: "2.0", - error: { - code: -32000, - message: "Bad Request: No valid session ID provided", - }, - id: req?.body?.id, - }); - return; - } - // Handle the request with existing transport - no need to reconnect - // The existing transport is already connected to the server - await transport.handleRequest(req, res); - } catch (error) { - console.log("Error handling MCP request:", error); - if (!res.headersSent) { - res.status(500).json({ - jsonrpc: "2.0", - error: { - code: -32603, - message: "Internal server error", - }, - id: req?.body?.id, - }); - return; + // Handle the request with existing transport - no need to reconnect + // The existing transport is already connected to the server + await transport.handleRequest(req, res); + } catch (error) { + console.log('Error handling MCP request:', error); + if (!res.headersSent) { + res.status(500).json({ + jsonrpc: '2.0', + error: { + code: -32603, + message: 'Internal server error' + }, + id: req?.body?.id + }); + return; + } } - } }); // Handle GET requests for SSE streams -app.get("/mcp", async (req: Request, res: Response) => { - console.log("Received MCP GET request"); - const sessionId = req.headers["mcp-session-id"] as string | undefined; - if (!sessionId || !transports.has(sessionId)) { - res.status(400).json({ - jsonrpc: "2.0", - error: { - code: -32000, - message: "Bad Request: No valid session ID provided", - }, - id: req?.body?.id, - }); - return; - } - - // Check for Last-Event-ID header for resumability - const lastEventId = req.headers["last-event-id"] as string | undefined; - if (lastEventId) { - console.log(`Client reconnecting with Last-Event-ID: ${lastEventId}`); - } else { - console.log(`Establishing new SSE stream for session ${sessionId}`); - } - - const transport = transports.get(sessionId); - await transport!.handleRequest(req, res); -}); +app.get('/mcp', async (req: Request, res: Response) => { + console.log('Received MCP GET request'); + const sessionId = req.headers['mcp-session-id'] as string | undefined; + if (!sessionId || !transports.has(sessionId)) { + res.status(400).json({ + jsonrpc: '2.0', + error: { + code: -32000, + message: 'Bad Request: No valid session ID provided' + }, + id: req?.body?.id + }); + return; + } + + // Check for Last-Event-ID header for resumability + const lastEventId = req.headers['last-event-id'] as string | undefined; + if (lastEventId) { + console.log(`Client reconnecting with Last-Event-ID: ${lastEventId}`); + } else { + console.log(`Establishing new SSE stream for session ${sessionId}`); + } -// Handle DELETE requests for session termination -app.delete("/mcp", async (req: Request, res: Response) => { - const sessionId = req.headers["mcp-session-id"] as string | undefined; - if (!sessionId || !transports.has(sessionId)) { - res.status(400).json({ - jsonrpc: "2.0", - error: { - code: -32000, - message: "Bad Request: No valid session ID provided", - }, - id: req?.body?.id, - }); - return; - } - - console.log(`Received session termination request for session ${sessionId}`); - - try { const transport = transports.get(sessionId); await transport!.handleRequest(req, res); - } catch (error) { - console.log("Error handling session termination:", error); - if (!res.headersSent) { - res.status(500).json({ - jsonrpc: "2.0", - error: { - code: -32603, - message: "Error handling session termination", - }, - id: req?.body?.id, - }); - return; +}); + +// Handle DELETE requests for session termination +app.delete('/mcp', async (req: Request, res: Response) => { + const sessionId = req.headers['mcp-session-id'] as string | undefined; + if (!sessionId || !transports.has(sessionId)) { + res.status(400).json({ + jsonrpc: '2.0', + error: { + code: -32000, + message: 'Bad Request: No valid session ID provided' + }, + id: req?.body?.id + }); + return; + } + + console.log(`Received session termination request for session ${sessionId}`); + + try { + const transport = transports.get(sessionId); + await transport!.handleRequest(req, res); + } catch (error) { + console.log('Error handling session termination:', error); + if (!res.headersSent) { + res.status(500).json({ + jsonrpc: '2.0', + error: { + code: -32603, + message: 'Error handling session termination' + }, + id: req?.body?.id + }); + return; + } } - } }); // Start the server const PORT = process.env.PORT || 3001; const server = app.listen(PORT, () => { - console.error(`MCP Streamable HTTP Server listening on port ${PORT}`); + console.error(`MCP Streamable HTTP Server listening on port ${PORT}`); }); // Handle server errors -server.on("error", (err: unknown) => { - const code = - typeof err === "object" && err !== null && "code" in err - ? (err as { code?: unknown }).code - : undefined; - if (code === "EADDRINUSE") { - console.error( - `Failed to start: Port ${PORT} is already in use. Set PORT to a free port or stop the conflicting process.` - ); - } else { - console.error("HTTP server encountered an error while starting:", err); - } - // Ensure a non-zero exit so npm reports the failure instead of silently exiting - process.exit(1); +server.on('error', (err: unknown) => { + const code = typeof err === 'object' && err !== null && 'code' in err ? (err as { code?: unknown }).code : undefined; + if (code === 'EADDRINUSE') { + console.error(`Failed to start: Port ${PORT} is already in use. Set PORT to a free port or stop the conflicting process.`); + } else { + console.error('HTTP server encountered an error while starting:', err); + } + // Ensure a non-zero exit so npm reports the failure instead of silently exiting + process.exit(1); }); // Handle server shutdown -process.on("SIGINT", async () => { - console.log("Shutting down server..."); - - // Close all active transports to properly clean up resources - for (const sessionId in transports) { - try { - console.log(`Closing transport for session ${sessionId}`); - await transports.get(sessionId)!.close(); - transports.delete(sessionId); - } catch (error) { - console.log(`Error closing transport for session ${sessionId}:`, error); +process.on('SIGINT', async () => { + console.log('Shutting down server...'); + + // Close all active transports to properly clean up resources + for (const sessionId in transports) { + try { + console.log(`Closing transport for session ${sessionId}`); + await transports.get(sessionId)!.close(); + transports.delete(sessionId); + } catch (error) { + console.log(`Error closing transport for session ${sessionId}:`, error); + } } - } - console.log("Server shutdown complete"); - process.exit(0); + console.log('Server shutdown complete'); + process.exit(0); }); diff --git a/src/everything/tsconfig.json b/src/everything/tsconfig.json index 829d52d66b..bc21186337 100644 --- a/src/everything/tsconfig.json +++ b/src/everything/tsconfig.json @@ -1,8 +1,8 @@ { - "extends": "../../tsconfig.json", - "compilerOptions": { - "outDir": "./dist", - "rootDir": "." - }, - "include": ["./**/*.ts"] + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "." + }, + "include": ["./**/*.ts"] } diff --git a/src/everything/vitest.config.ts b/src/everything/vitest.config.ts index d414ec8f52..f5889cbedf 100644 --- a/src/everything/vitest.config.ts +++ b/src/everything/vitest.config.ts @@ -1,14 +1,14 @@ import { defineConfig } from 'vitest/config'; export default defineConfig({ - test: { - globals: true, - environment: 'node', - include: ['**/__tests__/**/*.test.ts'], - coverage: { - provider: 'v8', - include: ['**/*.ts'], - exclude: ['**/__tests__/**', '**/dist/**'], - }, - }, + test: { + globals: true, + environment: 'node', + include: ['**/__tests__/**/*.test.ts'], + coverage: { + provider: 'v8', + include: ['**/*.ts'], + exclude: ['**/__tests__/**', '**/dist/**'] + } + } }); diff --git a/src/filesystem/__tests__/roots-utils.test.ts b/src/filesystem/__tests__/roots-utils.test.ts index 1a39483953..fbdf45d6d6 100644 --- a/src/filesystem/__tests__/roots-utils.test.ts +++ b/src/filesystem/__tests__/roots-utils.test.ts @@ -3,7 +3,7 @@ import { getValidRootDirectories } from '../roots-utils.js'; import { mkdtempSync, rmSync, mkdirSync, writeFileSync, realpathSync } from 'fs'; import { tmpdir } from 'os'; import { join } from 'path'; -import type { Root } from '@modelcontextprotocol/sdk/types.js'; +import type { Root } from "@modelcontextprotocol/server"; describe('getValidRootDirectories', () => { let testDir1: string; diff --git a/src/filesystem/__tests__/structured-content.test.ts b/src/filesystem/__tests__/structured-content.test.ts index 4b8f92b0a3..64403ea842 100644 --- a/src/filesystem/__tests__/structured-content.test.ts +++ b/src/filesystem/__tests__/structured-content.test.ts @@ -2,8 +2,8 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import * as fs from 'fs/promises'; import * as path from 'path'; import * as os from 'os'; -import { Client } from '@modelcontextprotocol/sdk/client/index.js'; -import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; +import { StdioClientTransport } from "@modelcontextprotocol/client/stdio"; +import { Client } from "@modelcontextprotocol/client"; import { spawn } from 'child_process'; /** diff --git a/src/filesystem/index.ts b/src/filesystem/index.ts index 7b67e63e58..c500e8b3e0 100644 --- a/src/filesystem/index.ts +++ b/src/filesystem/index.ts @@ -1,12 +1,8 @@ #!/usr/bin/env node -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { - CallToolResult, - RootsListChangedNotificationSchema, - type Root, -} from "@modelcontextprotocol/sdk/types.js"; +import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; +import { McpServer, CallToolResult } from "@modelcontextprotocol/server"; +import type { Root } from "@modelcontextprotocol/server"; import fs from "fs/promises"; import { createReadStream } from "fs"; import path from "path"; @@ -210,13 +206,14 @@ const readTextFileHandler = async (args: z.infer) }; }; +/* @mcp-codemod-error Could not verify `inputSchema` is a schema object. Raw shapes are deprecated in v2 — pass a Standard Schema object (e.g. z.object({ … })); no change is needed if it already is one. */ server.registerTool( "read_file", { title: "Read File (Deprecated)", description: "Read the complete contents of a file as text. DEPRECATED: Use read_text_file instead.", inputSchema: ReadTextFileArgsSchema.shape, - outputSchema: { content: z.string() }, + outputSchema: z.object({ content: z.string() }), annotations: { readOnlyHint: true } }, readTextFileHandler @@ -234,12 +231,12 @@ server.registerTool( "the first N lines of a file, or the 'tail' parameter to read only " + "the last N lines of a file. Operates on the file as text regardless of extension. " + "Only works within allowed directories.", - inputSchema: { - path: z.string(), - tail: z.number().optional().describe("If provided, returns only the last N lines of the file"), - head: z.number().optional().describe("If provided, returns only the first N lines of the file") - }, - outputSchema: { content: z.string() }, + inputSchema: z.object({ + path: z.string(), + tail: z.number().optional().describe("If provided, returns only the last N lines of the file"), + head: z.number().optional().describe("If provided, returns only the first N lines of the file") + }), + outputSchema: z.object({ content: z.string() }), annotations: { readOnlyHint: true } }, readTextFileHandler @@ -252,16 +249,16 @@ server.registerTool( description: "Read an image or audio file. Returns the base64 encoded data and MIME type. " + "Only works within allowed directories.", - inputSchema: { - path: z.string() - }, - outputSchema: { - content: z.array(z.object({ - type: z.enum(["image", "audio", "blob"]), - data: z.string(), - mimeType: z.string() - })) - }, + inputSchema: z.object({ + path: z.string() + }), + outputSchema: z.object({ + content: z.array(z.object({ + type: z.enum(["image", "audio", "blob"]), + data: z.string(), + mimeType: z.string() + })) + }), annotations: { readOnlyHint: true } }, async (args: z.infer) => { @@ -307,12 +304,12 @@ server.registerTool( "or compare multiple files. Each file's content is returned with its " + "path as a reference. Failed reads for individual files won't stop " + "the entire operation. Only works within allowed directories.", - inputSchema: { - paths: z.array(z.string()) - .min(1) - .describe("Array of file paths to read. Each path must be a string pointing to a valid file within allowed directories.") - }, - outputSchema: { content: z.string() }, + inputSchema: z.object({ + paths: z.array(z.string()) + .min(1) + .describe("Array of file paths to read. Each path must be a string pointing to a valid file within allowed directories.") + }), + outputSchema: z.object({ content: z.string() }), annotations: { readOnlyHint: true } }, async (args: z.infer) => { @@ -344,11 +341,11 @@ server.registerTool( "Create a new file or completely overwrite an existing file with new content. " + "Use with caution as it will overwrite existing files without warning. " + "Handles text content with proper encoding. Only works within allowed directories.", - inputSchema: { - path: z.string(), - content: z.string() - }, - outputSchema: { content: z.string() }, + inputSchema: z.object({ + path: z.string(), + content: z.string() + }), + outputSchema: z.object({ content: z.string() }), annotations: { readOnlyHint: false, idempotentHint: true, destructiveHint: true } }, async (args: z.infer) => { @@ -370,15 +367,15 @@ server.registerTool( "Make line-based edits to a text file. Each edit replaces exact line sequences " + "with new content. Returns a git-style diff showing the changes made. " + "Only works within allowed directories.", - inputSchema: { - path: z.string(), - edits: z.array(z.object({ - oldText: z.string().describe("Text to search for - must match exactly"), - newText: z.string().describe("Text to replace with") - })), - dryRun: z.boolean().default(false).describe("Preview changes using git-style diff format") - }, - outputSchema: { content: z.string() }, + inputSchema: z.object({ + path: z.string(), + edits: z.array(z.object({ + oldText: z.string().describe("Text to search for - must match exactly"), + newText: z.string().describe("Text to replace with") + })), + dryRun: z.boolean().default(false).describe("Preview changes using git-style diff format") + }), + outputSchema: z.object({ content: z.string() }), annotations: { readOnlyHint: false, idempotentHint: false, destructiveHint: true } }, async (args: z.infer) => { @@ -400,10 +397,10 @@ server.registerTool( "nested directories in one operation. If the directory already exists, " + "this operation will succeed silently. Perfect for setting up directory " + "structures for projects or ensuring required paths exist. Only works within allowed directories.", - inputSchema: { - path: z.string() - }, - outputSchema: { content: z.string() }, + inputSchema: z.object({ + path: z.string() + }), + outputSchema: z.object({ content: z.string() }), annotations: { readOnlyHint: false, idempotentHint: true, destructiveHint: false } }, async (args: z.infer) => { @@ -426,10 +423,10 @@ server.registerTool( "Results clearly distinguish between files and directories with [FILE] and [DIR] " + "prefixes. This tool is essential for understanding directory structure and " + "finding specific files within a directory. Only works within allowed directories.", - inputSchema: { - path: z.string() - }, - outputSchema: { content: z.string() }, + inputSchema: z.object({ + path: z.string() + }), + outputSchema: z.object({ content: z.string() }), annotations: { readOnlyHint: true } }, async (args: z.infer) => { @@ -454,11 +451,11 @@ server.registerTool( "Results clearly distinguish between files and directories with [FILE] and [DIR] " + "prefixes. This tool is useful for understanding directory structure and " + "finding specific files within a directory. Only works within allowed directories.", - inputSchema: { - path: z.string(), - sortBy: z.enum(["name", "size"]).optional().default("name").describe("Sort entries by name or size") - }, - outputSchema: { content: z.string() }, + inputSchema: z.object({ + path: z.string(), + sortBy: z.enum(["name", "size"]).optional().default("name").describe("Sort entries by name or size") + }), + outputSchema: z.object({ content: z.string() }), annotations: { readOnlyHint: true } }, async (args: z.infer) => { @@ -533,11 +530,11 @@ server.registerTool( "Each entry includes 'name', 'type' (file/directory), and 'children' for directories. " + "Files have no children array, while directories always have a children array (which may be empty). " + "The output is formatted with 2-space indentation for readability. Only works within allowed directories.", - inputSchema: { - path: z.string(), - excludePatterns: z.array(z.string()).optional().default([]) - }, - outputSchema: { content: z.string() }, + inputSchema: z.object({ + path: z.string(), + excludePatterns: z.array(z.string()).optional().default([]) + }), + outputSchema: z.object({ content: z.string() }), annotations: { readOnlyHint: true } }, async (args: z.infer) => { @@ -603,11 +600,11 @@ server.registerTool( "and rename them in a single operation. If the destination exists, the " + "operation will fail. Works across different directories and can be used " + "for simple renaming within the same directory. Both source and destination must be within allowed directories.", - inputSchema: { - source: z.string(), - destination: z.string() - }, - outputSchema: { content: z.string() }, + inputSchema: z.object({ + source: z.string(), + destination: z.string() + }), + outputSchema: z.object({ content: z.string() }), annotations: { readOnlyHint: false, idempotentHint: false, destructiveHint: true } }, async (args: z.infer) => { @@ -633,12 +630,12 @@ server.registerTool( "Use pattern like '*.ext' to match files in current directory, and '**/*.ext' to match files in all subdirectories. " + "Returns full paths to all matching items. Great for finding files when you don't know their exact location. " + "Only searches within allowed directories.", - inputSchema: { - path: z.string(), - pattern: z.string(), - excludePatterns: z.array(z.string()).optional().default([]) - }, - outputSchema: { content: z.string() }, + inputSchema: z.object({ + path: z.string(), + pattern: z.string(), + excludePatterns: z.array(z.string()).optional().default([]) + }), + outputSchema: z.object({ content: z.string() }), annotations: { readOnlyHint: true } }, async (args: z.infer) => { @@ -661,10 +658,10 @@ server.registerTool( "information including size, creation time, last modified time, permissions, " + "and type. This tool is perfect for understanding file characteristics " + "without reading the actual content. Only works within allowed directories.", - inputSchema: { - path: z.string() - }, - outputSchema: { content: z.string() }, + inputSchema: z.object({ + path: z.string() + }), + outputSchema: z.object({ content: z.string() }), annotations: { readOnlyHint: true } }, async (args: z.infer) => { @@ -689,8 +686,8 @@ server.registerTool( "Subdirectories within these allowed directories are also accessible. " + "Use this to understand which directories and their nested paths are available " + "before trying to access files.", - inputSchema: {}, - outputSchema: { content: z.string() }, + inputSchema: z.object({}), + outputSchema: z.object({ content: z.string() }), annotations: { readOnlyHint: true } }, async () => { @@ -715,7 +712,7 @@ async function updateAllowedDirectoriesFromRoots(requestedRoots: Root[]) { } // Handles dynamic roots updates during runtime, when client sends "roots/list_changed" notification, server fetches the updated roots and replaces all allowed directories with the new roots. -server.server.setNotificationHandler(RootsListChangedNotificationSchema, async () => { +server.server.setNotificationHandler('notifications/roots/list_changed', async () => { try { // Request the updated roots list from the client const response = await server.server.listRoots(); diff --git a/src/filesystem/package.json b/src/filesystem/package.json index c943ce24d3..12dca82b3a 100644 --- a/src/filesystem/package.json +++ b/src/filesystem/package.json @@ -25,10 +25,11 @@ "test": "vitest run --coverage" }, "dependencies": { - "@modelcontextprotocol/sdk": "^1.29.0", "diff": "^8.0.3", "glob": "^10.5.0", - "minimatch": "^10.0.1" + "minimatch": "^10.0.1", + "@modelcontextprotocol/server": "^2.0.0-alpha.3", + "@modelcontextprotocol/client": "^2.0.0-alpha.3" }, "devDependencies": { "@types/diff": "^5.0.9", diff --git a/src/filesystem/roots-utils.ts b/src/filesystem/roots-utils.ts index 5e26bb246b..252b9400cf 100644 --- a/src/filesystem/roots-utils.ts +++ b/src/filesystem/roots-utils.ts @@ -2,7 +2,7 @@ import { promises as fs, type Stats } from 'fs'; import path from 'path'; import os from 'os'; import { normalizePath } from './path-utils.js'; -import type { Root } from '@modelcontextprotocol/sdk/types.js'; +import type { Root } from "@modelcontextprotocol/server"; import { fileURLToPath } from "url"; /** diff --git a/src/sequentialthinking/index.ts b/src/sequentialthinking/index.ts index 217845bb3d..7ba2b72fdf 100644 --- a/src/sequentialthinking/index.ts +++ b/src/sequentialthinking/index.ts @@ -1,7 +1,7 @@ #!/usr/bin/env node -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; +import { McpServer } from "@modelcontextprotocol/server"; import { z } from "zod"; import { SequentialThinkingServer } from './lib.js'; @@ -80,30 +80,30 @@ You should: 9. Repeat the process until satisfied with the solution 10. Provide a single, ideally correct answer as the final output 11. Only set nextThoughtNeeded to false when truly done and a satisfactory answer is reached`, - inputSchema: { - thought: z.string().describe("Your current thinking step"), - nextThoughtNeeded: coercedBoolean.describe("Whether another thought step is needed"), - thoughtNumber: z.coerce.number().int().min(1).describe("Current thought number (numeric value, e.g., 1, 2, 3)"), - totalThoughts: z.coerce.number().int().min(1).describe("Estimated total thoughts needed (numeric value, e.g., 5, 10)"), - isRevision: coercedBoolean.optional().describe("Whether this revises previous thinking"), - revisesThought: z.coerce.number().int().min(1).optional().describe("Which thought is being reconsidered"), - branchFromThought: z.coerce.number().int().min(1).optional().describe("Branching point thought number"), - branchId: z.string().optional().describe("Branch identifier"), - needsMoreThoughts: coercedBoolean.optional().describe("If more thoughts are needed") - }, + inputSchema: z.object({ + thought: z.string().describe("Your current thinking step"), + nextThoughtNeeded: coercedBoolean.describe("Whether another thought step is needed"), + thoughtNumber: z.coerce.number().int().min(1).describe("Current thought number (numeric value, e.g., 1, 2, 3)"), + totalThoughts: z.coerce.number().int().min(1).describe("Estimated total thoughts needed (numeric value, e.g., 5, 10)"), + isRevision: coercedBoolean.optional().describe("Whether this revises previous thinking"), + revisesThought: z.coerce.number().int().min(1).optional().describe("Which thought is being reconsidered"), + branchFromThought: z.coerce.number().int().min(1).optional().describe("Branching point thought number"), + branchId: z.string().optional().describe("Branch identifier"), + needsMoreThoughts: coercedBoolean.optional().describe("If more thoughts are needed") + }), annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, }, - outputSchema: { - thoughtNumber: z.number(), - totalThoughts: z.number(), - nextThoughtNeeded: z.boolean(), - branches: z.array(z.string()), - thoughtHistoryLength: z.number() - }, + outputSchema: z.object({ + thoughtNumber: z.number(), + totalThoughts: z.number(), + nextThoughtNeeded: z.boolean(), + branches: z.array(z.string()), + thoughtHistoryLength: z.number() + }), }, async (args) => { const result = thinkingServer.processThought(args); diff --git a/src/sequentialthinking/package.json b/src/sequentialthinking/package.json index a88de803b5..f6ce8e5b02 100644 --- a/src/sequentialthinking/package.json +++ b/src/sequentialthinking/package.json @@ -25,9 +25,9 @@ "test": "vitest run --coverage" }, "dependencies": { - "@modelcontextprotocol/sdk": "^1.29.0", "chalk": "^5.3.0", - "yargs": "^17.7.2" + "yargs": "^17.7.2", + "@modelcontextprotocol/server": "^2.0.0-alpha.3" }, "devDependencies": { "@types/node": "^22", From 5a5452b516cd40f923e88072e078727f9c09a271 Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Wed, 1 Jul 2026 12:48:34 +0300 Subject: [PATCH 2/4] prettier fix --- src/everything/AGENTS.md | 66 +- src/everything/__tests__/prompts.test.ts | 295 +-- .../__tests__/registrations.test.ts | 256 +- src/everything/__tests__/resources.test.ts | 580 ++--- src/everything/__tests__/server.test.ts | 58 +- src/everything/__tests__/tools.test.ts | 2080 +++++++++-------- src/everything/docs/architecture.md | 6 +- src/everything/docs/extension.md | 12 +- src/everything/docs/features.md | 84 +- src/everything/docs/how-it-works.md | 34 +- src/everything/docs/instructions.md | 22 +- src/everything/docs/startup.md | 102 +- src/everything/docs/structure.md | 198 +- src/everything/index.ts | 62 +- src/everything/package.json | 98 +- src/everything/prompts/args.ts | 62 +- src/everything/prompts/completions.ts | 90 +- src/everything/prompts/index.ts | 18 +- src/everything/prompts/resource.ts | 137 +- src/everything/prompts/simple.ts | 36 +- src/everything/resources/files.ts | 110 +- src/everything/resources/index.ts | 36 +- src/everything/resources/session.ts | 84 +- src/everything/resources/subscriptions.ts | 168 +- src/everything/resources/templates.ts | 172 +- src/everything/server/index.ts | 147 +- src/everything/server/logging.ts | 101 +- src/everything/server/roots.ts | 108 +- src/everything/tools/echo.ts | 38 +- src/everything/tools/get-annotated-message.ts | 128 +- src/everything/tools/get-env.ts | 43 +- src/everything/tools/get-resource-links.ts | 99 +- .../tools/get-resource-reference.ts | 125 +- src/everything/tools/get-roots-list.ts | 127 +- .../tools/get-structured-content.ts | 111 +- src/everything/tools/get-sum.ts | 52 +- src/everything/tools/get-tiny-image.ts | 62 +- src/everything/tools/gzip-file-as-resource.ts | 329 +-- src/everything/tools/index.ts | 76 +- .../tools/toggle-simulated-logging.ts | 63 +- .../tools/toggle-subscriber-updates.ts | 63 +- .../trigger-elicitation-request-async.ts | 448 ++-- .../tools/trigger-elicitation-request.ts | 377 +-- .../tools/trigger-long-running-operation.ts | 95 +- .../tools/trigger-sampling-request-async.ts | 384 +-- .../tools/trigger-sampling-request.ts | 123 +- .../tools/trigger-url-elicitation.ts | 298 +-- src/everything/transports/sse.ts | 105 +- src/everything/transports/stdio.ts | 32 +- src/everything/transports/streamableHttp.ts | 387 +-- src/everything/tsconfig.json | 12 +- src/everything/vitest.config.ts | 22 +- 52 files changed, 4674 insertions(+), 4147 deletions(-) diff --git a/src/everything/AGENTS.md b/src/everything/AGENTS.md index 85ec40cf7d..c4a6df1348 100644 --- a/src/everything/AGENTS.md +++ b/src/everything/AGENTS.md @@ -2,30 +2,30 @@ ## Build, Test & Run Commands -- Build: `npm run build` - Compiles TypeScript to JavaScript -- Watch mode: `npm run watch` - Watches for changes and rebuilds automatically -- Run STDIO server: `npm run start:stdio` - Starts the MCP server using stdio transport -- Run SSE server: `npm run start:sse` - Starts the MCP server with SSE transport -- Run StreamableHttp server: `npm run start:streamableHttp` - Starts the MCP server with StreamableHttp transport -- Prepare release: `npm run prepare` - Builds the project for publishing +- Build: `npm run build` - Compiles TypeScript to JavaScript +- Watch mode: `npm run watch` - Watches for changes and rebuilds automatically +- Run STDIO server: `npm run start:stdio` - Starts the MCP server using stdio transport +- Run SSE server: `npm run start:sse` - Starts the MCP server with SSE transport +- Run StreamableHttp server: `npm run start:streamableHttp` - Starts the MCP server with StreamableHttp transport +- Prepare release: `npm run prepare` - Builds the project for publishing ## Code Style Guidelines -- Use ES modules with `.js` extension in import paths -- Strictly type all functions and variables with TypeScript -- Follow zod schema patterns for tool input validation -- Prefer async/await over callbacks and Promise chains -- Place all imports at top of file, grouped by external then internal -- Use descriptive variable names that clearly indicate purpose -- Implement proper cleanup for timers and resources in server shutdown -- Handle errors with try/catch blocks and provide clear error messages -- Use consistent indentation (2 spaces) and trailing commas in multi-line objects -- Match existing code style, import order, and module layout in the respective folder. -- Use camelCase for variables/functions, -- Use PascalCase for types/classes, -- Use UPPER_CASE for constants -- Use kebab-case for file names and registered tools, prompts, and resources. -- Use verbs for tool names, e.g., `get-annotated-message` instead of `annotated-message` +- Use ES modules with `.js` extension in import paths +- Strictly type all functions and variables with TypeScript +- Follow zod schema patterns for tool input validation +- Prefer async/await over callbacks and Promise chains +- Place all imports at top of file, grouped by external then internal +- Use descriptive variable names that clearly indicate purpose +- Implement proper cleanup for timers and resources in server shutdown +- Handle errors with try/catch blocks and provide clear error messages +- Use consistent indentation (2 spaces) and trailing commas in multi-line objects +- Match existing code style, import order, and module layout in the respective folder. +- Use camelCase for variables/functions, +- Use PascalCase for types/classes, +- Use UPPER_CASE for constants +- Use kebab-case for file names and registered tools, prompts, and resources. +- Use verbs for tool names, e.g., `get-annotated-message` instead of `annotated-message` ## Extending the Server @@ -35,18 +35,18 @@ The server factory is `src/everything/server/index.ts` and registers all feature ### High-level -- Tools live under `src/everything/tools/` and are registered via `registerTools(server)`. -- Resources live under `src/everything/resources/` and are registered via `registerResources(server)`. -- Prompts live under `src/everything/prompts/` and are registered via `registerPrompts(server)`. -- Subscriptions and simulated update routines are under `src/everything/resources/subscriptions.ts`. -- Logging helpers are under `src/everything/server/logging.ts`. -- Transport managers are under `src/everything/transports/`. +- Tools live under `src/everything/tools/` and are registered via `registerTools(server)`. +- Resources live under `src/everything/resources/` and are registered via `registerResources(server)`. +- Prompts live under `src/everything/prompts/` and are registered via `registerPrompts(server)`. +- Subscriptions and simulated update routines are under `src/everything/resources/subscriptions.ts`. +- Logging helpers are under `src/everything/server/logging.ts`. +- Transport managers are under `src/everything/transports/`. ### When adding a new feature -- Follow the existing file/module pattern in its folder (naming, exports, and registration function). -- Export a `registerX(server)` function that registers new items with the MCP SDK in the same style as existing ones. -- Wire your new module into the central index (e.g., update `tools/index.ts`, `resources/index.ts`, or `prompts/index.ts`). -- Ensure schemas (for tools) are accurate JSON Schema and include helpful descriptions and examples. - `server/index.ts` and usages in `logging.ts` and `subscriptions.ts`. -- Keep the docs in `src/everything/docs/` up to date if you add or modify noteworthy features. +- Follow the existing file/module pattern in its folder (naming, exports, and registration function). +- Export a `registerX(server)` function that registers new items with the MCP SDK in the same style as existing ones. +- Wire your new module into the central index (e.g., update `tools/index.ts`, `resources/index.ts`, or `prompts/index.ts`). +- Ensure schemas (for tools) are accurate JSON Schema and include helpful descriptions and examples. + `server/index.ts` and usages in `logging.ts` and `subscriptions.ts`. +- Keep the docs in `src/everything/docs/` up to date if you add or modify noteworthy features. diff --git a/src/everything/__tests__/prompts.test.ts b/src/everything/__tests__/prompts.test.ts index 20b6728208..8dca73e034 100644 --- a/src/everything/__tests__/prompts.test.ts +++ b/src/everything/__tests__/prompts.test.ts @@ -1,167 +1,186 @@ -import { describe, it, expect, vi } from 'vitest'; -import { McpServer } from '@modelcontextprotocol/server'; -import { registerSimplePrompt } from '../prompts/simple.js'; -import { registerArgumentsPrompt } from '../prompts/args.js'; -import { registerPromptWithCompletions } from '../prompts/completions.js'; -import { registerEmbeddedResourcePrompt } from '../prompts/resource.js'; +import { describe, it, expect, vi } from "vitest"; +import { McpServer } from "@modelcontextprotocol/server"; +import { registerSimplePrompt } from "../prompts/simple.js"; +import { registerArgumentsPrompt } from "../prompts/args.js"; +import { registerPromptWithCompletions } from "../prompts/completions.js"; +import { registerEmbeddedResourcePrompt } from "../prompts/resource.js"; // Helper to capture registered prompt handlers function createMockServer() { - const handlers: Map = new Map(); - const configs: Map = new Map(); + const handlers: Map = new Map(); + const configs: Map = new Map(); - const mockServer = { - registerPrompt: vi.fn((name: string, config: any, handler: Function) => { - handlers.set(name, handler); - configs.set(name, config); - }) - } as unknown as McpServer; + const mockServer = { + registerPrompt: vi.fn((name: string, config: any, handler: Function) => { + handlers.set(name, handler); + configs.set(name, config); + }), + } as unknown as McpServer; - return { mockServer, handlers, configs }; + return { mockServer, handlers, configs }; } -describe('Prompts', () => { - describe('simple-prompt', () => { - it('should return fixed message with no arguments', () => { - const { mockServer, handlers } = createMockServer(); - registerSimplePrompt(mockServer); - - const handler = handlers.get('simple-prompt')!; - const result = handler(); - - expect(result).toEqual({ - messages: [ - { - role: 'user', - content: { - type: 'text', - text: 'This is a simple prompt without arguments.' - } - } - ] - }); - }); +describe("Prompts", () => { + describe("simple-prompt", () => { + it("should return fixed message with no arguments", () => { + const { mockServer, handlers } = createMockServer(); + registerSimplePrompt(mockServer); + + const handler = handlers.get("simple-prompt")!; + const result = handler(); + + expect(result).toEqual({ + messages: [ + { + role: "user", + content: { + type: "text", + text: "This is a simple prompt without arguments.", + }, + }, + ], + }); }); + }); - describe('args-prompt', () => { - it('should include city in message', () => { - const { mockServer, handlers } = createMockServer(); - registerArgumentsPrompt(mockServer); + describe("args-prompt", () => { + it("should include city in message", () => { + const { mockServer, handlers } = createMockServer(); + registerArgumentsPrompt(mockServer); - const handler = handlers.get('args-prompt')!; - const result = handler({ city: 'San Francisco' }); + const handler = handlers.get("args-prompt")!; + const result = handler({ city: "San Francisco" }); - expect(result.messages[0].content.text).toBe("What's weather in San Francisco?"); - }); + expect(result.messages[0].content.text).toBe( + "What's weather in San Francisco?" + ); + }); + + it("should include city and state in message", () => { + const { mockServer, handlers } = createMockServer(); + registerArgumentsPrompt(mockServer); + + const handler = handlers.get("args-prompt")!; + const result = handler({ city: "San Francisco", state: "California" }); + + expect(result.messages[0].content.text).toBe( + "What's weather in San Francisco, California?" + ); + }); + + it("should handle city only (optional state omitted)", () => { + const { mockServer, handlers } = createMockServer(); + registerArgumentsPrompt(mockServer); + + const handler = handlers.get("args-prompt")!; + const result = handler({ city: "New York" }); + + expect(result.messages[0].content.text).toBe( + "What's weather in New York?" + ); + expect(result.messages[0].content.text).not.toContain(","); + expect(result.messages[0].role).toBe("user"); + expect(result.messages[0].content.type).toBe("text"); + }); + }); - it('should include city and state in message', () => { - const { mockServer, handlers } = createMockServer(); - registerArgumentsPrompt(mockServer); + describe("completable-prompt", () => { + it("should generate promotion message with department and name", () => { + const { mockServer, handlers } = createMockServer(); + registerPromptWithCompletions(mockServer); - const handler = handlers.get('args-prompt')!; - const result = handler({ city: 'San Francisco', state: 'California' }); + const handler = handlers.get("completable-prompt")!; + const result = handler({ department: "Engineering", name: "Alice" }); - expect(result.messages[0].content.text).toBe("What's weather in San Francisco, California?"); - }); + expect(result.messages[0].content.text).toBe( + "Please promote Alice to the head of the Engineering team." + ); + }); + + it("should work with different departments", () => { + const { mockServer, handlers } = createMockServer(); + registerPromptWithCompletions(mockServer); - it('should handle city only (optional state omitted)', () => { - const { mockServer, handlers } = createMockServer(); - registerArgumentsPrompt(mockServer); + const handler = handlers.get("completable-prompt")!; - const handler = handlers.get('args-prompt')!; - const result = handler({ city: 'New York' }); + const salesResult = handler({ department: "Sales", name: "David" }); + expect(salesResult.messages[0].content.text).toContain("Sales"); + expect(salesResult.messages[0].content.text).toContain("David"); + expect(salesResult.messages[0].role).toBe("user"); - expect(result.messages[0].content.text).toBe("What's weather in New York?"); - expect(result.messages[0].content.text).not.toContain(','); - expect(result.messages[0].role).toBe('user'); - expect(result.messages[0].content.type).toBe('text'); - }); + const marketingResult = handler({ + department: "Marketing", + name: "Grace", + }); + expect(marketingResult.messages[0].content.text).toContain("Marketing"); + expect(marketingResult.messages[0].content.text).toContain("Grace"); }); + }); - describe('completable-prompt', () => { - it('should generate promotion message with department and name', () => { - const { mockServer, handlers } = createMockServer(); - registerPromptWithCompletions(mockServer); + describe("resource-prompt", () => { + it("should return text resource reference", () => { + const { mockServer, handlers } = createMockServer(); + registerEmbeddedResourcePrompt(mockServer); - const handler = handlers.get('completable-prompt')!; - const result = handler({ department: 'Engineering', name: 'Alice' }); + const handler = handlers.get("resource-prompt")!; + const result = handler({ resourceType: "Text", resourceId: "1" }); - expect(result.messages[0].content.text).toBe('Please promote Alice to the head of the Engineering team.'); - }); + expect(result.messages).toHaveLength(2); + expect(result.messages[0].content.text).toContain("Text"); + expect(result.messages[0].content.text).toContain("1"); + expect(result.messages[1].content.type).toBe("resource"); + expect(result.messages[1].content.resource.uri).toContain("text/1"); + }); - it('should work with different departments', () => { - const { mockServer, handlers } = createMockServer(); - registerPromptWithCompletions(mockServer); + it("should return blob resource reference", () => { + const { mockServer, handlers } = createMockServer(); + registerEmbeddedResourcePrompt(mockServer); - const handler = handlers.get('completable-prompt')!; + const handler = handlers.get("resource-prompt")!; + const result = handler({ resourceType: "Blob", resourceId: "5" }); + + expect(result.messages[0].content.text).toContain("Blob"); + expect(result.messages[1].content.resource.uri).toContain("blob/5"); + }); - const salesResult = handler({ department: 'Sales', name: 'David' }); - expect(salesResult.messages[0].content.text).toContain('Sales'); - expect(salesResult.messages[0].content.text).toContain('David'); - expect(salesResult.messages[0].role).toBe('user'); + it("should reject invalid resource type", () => { + const { mockServer, handlers } = createMockServer(); + registerEmbeddedResourcePrompt(mockServer); - const marketingResult = handler({ department: 'Marketing', name: 'Grace' }); - expect(marketingResult.messages[0].content.text).toContain('Marketing'); - expect(marketingResult.messages[0].content.text).toContain('Grace'); - }); + const handler = handlers.get("resource-prompt")!; + expect(() => + handler({ resourceType: "Invalid", resourceId: "1" }) + ).toThrow("Invalid resourceType"); }); - describe('resource-prompt', () => { - it('should return text resource reference', () => { - const { mockServer, handlers } = createMockServer(); - registerEmbeddedResourcePrompt(mockServer); - - const handler = handlers.get('resource-prompt')!; - const result = handler({ resourceType: 'Text', resourceId: '1' }); - - expect(result.messages).toHaveLength(2); - expect(result.messages[0].content.text).toContain('Text'); - expect(result.messages[0].content.text).toContain('1'); - expect(result.messages[1].content.type).toBe('resource'); - expect(result.messages[1].content.resource.uri).toContain('text/1'); - }); - - it('should return blob resource reference', () => { - const { mockServer, handlers } = createMockServer(); - registerEmbeddedResourcePrompt(mockServer); - - const handler = handlers.get('resource-prompt')!; - const result = handler({ resourceType: 'Blob', resourceId: '5' }); - - expect(result.messages[0].content.text).toContain('Blob'); - expect(result.messages[1].content.resource.uri).toContain('blob/5'); - }); - - it('should reject invalid resource type', () => { - const { mockServer, handlers } = createMockServer(); - registerEmbeddedResourcePrompt(mockServer); - - const handler = handlers.get('resource-prompt')!; - expect(() => handler({ resourceType: 'Invalid', resourceId: '1' })).toThrow('Invalid resourceType'); - }); - - it('should reject invalid resource ID', () => { - const { mockServer, handlers } = createMockServer(); - registerEmbeddedResourcePrompt(mockServer); - - const handler = handlers.get('resource-prompt')!; - expect(() => handler({ resourceType: 'Text', resourceId: '-1' })).toThrow('Invalid resourceId'); - expect(() => handler({ resourceType: 'Text', resourceId: '0' })).toThrow('Invalid resourceId'); - expect(() => handler({ resourceType: 'Text', resourceId: 'abc' })).toThrow('Invalid resourceId'); - }); - - it('should include both intro text and resource messages', () => { - const { mockServer, handlers } = createMockServer(); - registerEmbeddedResourcePrompt(mockServer); - - const handler = handlers.get('resource-prompt')!; - const result = handler({ resourceType: 'Text', resourceId: '3' }); - - expect(result.messages).toHaveLength(2); - expect(result.messages[0].role).toBe('user'); - expect(result.messages[0].content.type).toBe('text'); - expect(result.messages[1].role).toBe('user'); - expect(result.messages[1].content.type).toBe('resource'); - }); + it("should reject invalid resource ID", () => { + const { mockServer, handlers } = createMockServer(); + registerEmbeddedResourcePrompt(mockServer); + + const handler = handlers.get("resource-prompt")!; + expect(() => handler({ resourceType: "Text", resourceId: "-1" })).toThrow( + "Invalid resourceId" + ); + expect(() => handler({ resourceType: "Text", resourceId: "0" })).toThrow( + "Invalid resourceId" + ); + expect(() => + handler({ resourceType: "Text", resourceId: "abc" }) + ).toThrow("Invalid resourceId"); + }); + + it("should include both intro text and resource messages", () => { + const { mockServer, handlers } = createMockServer(); + registerEmbeddedResourcePrompt(mockServer); + + const handler = handlers.get("resource-prompt")!; + const result = handler({ resourceType: "Text", resourceId: "3" }); + + expect(result.messages).toHaveLength(2); + expect(result.messages[0].role).toBe("user"); + expect(result.messages[0].content.type).toBe("text"); + expect(result.messages[1].role).toBe("user"); + expect(result.messages[1].content.type).toBe("resource"); }); + }); }); diff --git a/src/everything/__tests__/registrations.test.ts b/src/everything/__tests__/registrations.test.ts index c87973db20..ba30682cf8 100644 --- a/src/everything/__tests__/registrations.test.ts +++ b/src/everything/__tests__/registrations.test.ts @@ -1,144 +1,152 @@ -import { describe, it, expect, vi } from 'vitest'; -import { McpServer } from '@modelcontextprotocol/server'; +import { describe, it, expect, vi } from "vitest"; +import { McpServer } from "@modelcontextprotocol/server"; // Create mock server function createMockServer() { - return { + return { + registerTool: vi.fn(), + registerPrompt: vi.fn(), + registerResource: vi.fn(), + server: { + getClientCapabilities: vi.fn(() => ({})), + setRequestHandler: vi.fn(), + }, + sendLoggingMessage: vi.fn(), + sendResourceUpdated: vi.fn(), + } as unknown as McpServer; +} + +describe("Registration Index Files", () => { + describe("tools/index.ts", () => { + it("should register all standard tools", async () => { + const { registerTools } = await import("../tools/index.js"); + const mockServer = createMockServer(); + + registerTools(mockServer); + + // Should register 12 standard tools (non-conditional) + expect(mockServer.registerTool).toHaveBeenCalledTimes(12); + + // Verify specific tools are registered + const registeredTools = (mockServer.registerTool as any).mock.calls.map( + (call: any[]) => call[0] + ); + expect(registeredTools).toContain("echo"); + expect(registeredTools).toContain("get-sum"); + expect(registeredTools).toContain("get-env"); + expect(registeredTools).toContain("get-tiny-image"); + expect(registeredTools).toContain("get-structured-content"); + expect(registeredTools).toContain("get-annotated-message"); + expect(registeredTools).toContain("trigger-long-running-operation"); + expect(registeredTools).toContain("get-resource-links"); + expect(registeredTools).toContain("get-resource-reference"); + expect(registeredTools).toContain("gzip-file-as-resource"); + expect(registeredTools).toContain("toggle-simulated-logging"); + expect(registeredTools).toContain("toggle-subscriber-updates"); + }); + + it("should register conditional tools based on capabilities", async () => { + const { registerConditionalTools } = await import("../tools/index.js"); + + // Server with all capabilities including experimental tasks API + const mockServerWithCapabilities = { registerTool: vi.fn(), - registerPrompt: vi.fn(), - registerResource: vi.fn(), server: { - getClientCapabilities: vi.fn(() => ({})), - setRequestHandler: vi.fn() + getClientCapabilities: vi.fn(() => ({ + roots: {}, + elicitation: { url: {} }, + sampling: {}, + })), }, - sendLoggingMessage: vi.fn(), - sendResourceUpdated: vi.fn() - } as unknown as McpServer; -} - -describe('Registration Index Files', () => { - describe('tools/index.ts', () => { - it('should register all standard tools', async () => { - const { registerTools } = await import('../tools/index.js'); - const mockServer = createMockServer(); - - registerTools(mockServer); - - // Should register 12 standard tools (non-conditional) - expect(mockServer.registerTool).toHaveBeenCalledTimes(12); - - // Verify specific tools are registered - const registeredTools = (mockServer.registerTool as any).mock.calls.map((call: any[]) => call[0]); - expect(registeredTools).toContain('echo'); - expect(registeredTools).toContain('get-sum'); - expect(registeredTools).toContain('get-env'); - expect(registeredTools).toContain('get-tiny-image'); - expect(registeredTools).toContain('get-structured-content'); - expect(registeredTools).toContain('get-annotated-message'); - expect(registeredTools).toContain('trigger-long-running-operation'); - expect(registeredTools).toContain('get-resource-links'); - expect(registeredTools).toContain('get-resource-reference'); - expect(registeredTools).toContain('gzip-file-as-resource'); - expect(registeredTools).toContain('toggle-simulated-logging'); - expect(registeredTools).toContain('toggle-subscriber-updates'); - }); - - it('should register conditional tools based on capabilities', async () => { - const { registerConditionalTools } = await import('../tools/index.js'); - - // Server with all capabilities including experimental tasks API - const mockServerWithCapabilities = { - registerTool: vi.fn(), - server: { - getClientCapabilities: vi.fn(() => ({ - roots: {}, - elicitation: { url: {} }, - sampling: {} - })) - }, - experimental: { - tasks: { - registerToolTask: vi.fn() - } - } - } as unknown as McpServer; - - registerConditionalTools(mockServerWithCapabilities); - - // Should register 4 conditional tools via registerTool when all capabilities - // are present. Task-based tools register via registerToolTask (counted separately), - // so they are not included in this registerTool count. - expect(mockServerWithCapabilities.registerTool).toHaveBeenCalledTimes(4); - - const registeredTools = (mockServerWithCapabilities.registerTool as any).mock.calls.map((call: any[]) => call[0]); - expect(registeredTools).toContain('get-roots-list'); - expect(registeredTools).toContain('trigger-elicitation-request'); - expect(registeredTools).toContain('trigger-url-elicitation'); - expect(registeredTools).toContain('trigger-sampling-request'); - }); - - it('should not register conditional tools when capabilities missing', async () => { - const { registerConditionalTools } = await import('../tools/index.js'); - - const mockServerNoCapabilities = { - registerTool: vi.fn(), - server: { - getClientCapabilities: vi.fn(() => ({})) - }, - experimental: { - tasks: { - registerToolTask: vi.fn() - } - } - } as unknown as McpServer; - - registerConditionalTools(mockServerNoCapabilities); - - // Should not register any capability-gated tools when capabilities are missing - expect(mockServerNoCapabilities.registerTool).not.toHaveBeenCalled(); - }); + experimental: { + tasks: { + registerToolTask: vi.fn(), + }, + }, + } as unknown as McpServer; + + registerConditionalTools(mockServerWithCapabilities); + + // Should register 4 conditional tools via registerTool when all capabilities + // are present. Task-based tools register via registerToolTask (counted separately), + // so they are not included in this registerTool count. + expect(mockServerWithCapabilities.registerTool).toHaveBeenCalledTimes(4); + + const registeredTools = ( + mockServerWithCapabilities.registerTool as any + ).mock.calls.map((call: any[]) => call[0]); + expect(registeredTools).toContain("get-roots-list"); + expect(registeredTools).toContain("trigger-elicitation-request"); + expect(registeredTools).toContain("trigger-url-elicitation"); + expect(registeredTools).toContain("trigger-sampling-request"); }); - describe('prompts/index.ts', () => { - it('should register all prompts', async () => { - const { registerPrompts } = await import('../prompts/index.js'); - const mockServer = createMockServer(); + it("should not register conditional tools when capabilities missing", async () => { + const { registerConditionalTools } = await import("../tools/index.js"); - registerPrompts(mockServer); + const mockServerNoCapabilities = { + registerTool: vi.fn(), + server: { + getClientCapabilities: vi.fn(() => ({})), + }, + experimental: { + tasks: { + registerToolTask: vi.fn(), + }, + }, + } as unknown as McpServer; - // Should register 4 prompts - expect(mockServer.registerPrompt).toHaveBeenCalledTimes(4); + registerConditionalTools(mockServerNoCapabilities); - const registeredPrompts = (mockServer.registerPrompt as any).mock.calls.map((call: any[]) => call[0]); - expect(registeredPrompts).toContain('simple-prompt'); - expect(registeredPrompts).toContain('args-prompt'); - expect(registeredPrompts).toContain('completable-prompt'); - expect(registeredPrompts).toContain('resource-prompt'); - }); + // Should not register any capability-gated tools when capabilities are missing + expect(mockServerNoCapabilities.registerTool).not.toHaveBeenCalled(); }); + }); - describe('resources/index.ts', () => { - it('should register resource templates', async () => { - const { registerResources } = await import('../resources/index.js'); - const mockServer = createMockServer(); + describe("prompts/index.ts", () => { + it("should register all prompts", async () => { + const { registerPrompts } = await import("../prompts/index.js"); + const mockServer = createMockServer(); - registerResources(mockServer); + registerPrompts(mockServer); - // Should register at least the 2 resource templates (text and blob) plus file resources - expect(mockServer.registerResource).toHaveBeenCalled(); - const registeredResources = (mockServer.registerResource as any).mock.calls.map((call: any[]) => call[0]); - expect(registeredResources).toContain('Dynamic Text Resource'); - expect(registeredResources).toContain('Dynamic Blob Resource'); - }); + // Should register 4 prompts + expect(mockServer.registerPrompt).toHaveBeenCalledTimes(4); + + const registeredPrompts = ( + mockServer.registerPrompt as any + ).mock.calls.map((call: any[]) => call[0]); + expect(registeredPrompts).toContain("simple-prompt"); + expect(registeredPrompts).toContain("args-prompt"); + expect(registeredPrompts).toContain("completable-prompt"); + expect(registeredPrompts).toContain("resource-prompt"); + }); + }); + + describe("resources/index.ts", () => { + it("should register resource templates", async () => { + const { registerResources } = await import("../resources/index.js"); + const mockServer = createMockServer(); + + registerResources(mockServer); + + // Should register at least the 2 resource templates (text and blob) plus file resources + expect(mockServer.registerResource).toHaveBeenCalled(); + const registeredResources = ( + mockServer.registerResource as any + ).mock.calls.map((call: any[]) => call[0]); + expect(registeredResources).toContain("Dynamic Text Resource"); + expect(registeredResources).toContain("Dynamic Blob Resource"); + }); - it('should read instructions from file', async () => { - const { readInstructions } = await import('../resources/index.js'); + it("should read instructions from file", async () => { + const { readInstructions } = await import("../resources/index.js"); - const instructions = readInstructions(); + const instructions = readInstructions(); - // Should return a string (either content or error message) - expect(typeof instructions).toBe('string'); - expect(instructions.length).toBeGreaterThan(0); - }); + // Should return a string (either content or error message) + expect(typeof instructions).toBe("string"); + expect(instructions.length).toBeGreaterThan(0); }); + }); }); diff --git a/src/everything/__tests__/resources.test.ts b/src/everything/__tests__/resources.test.ts index b48f7e1607..1cf1cdde6d 100644 --- a/src/everything/__tests__/resources.test.ts +++ b/src/everything/__tests__/resources.test.ts @@ -1,309 +1,357 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import { McpServer, ResourceTemplate } from '@modelcontextprotocol/server'; +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { McpServer, ResourceTemplate } from "@modelcontextprotocol/server"; import { - textResource, - blobResource, - textResourceUri, - blobResourceUri, - RESOURCE_TYPE_TEXT, - RESOURCE_TYPE_BLOB, - RESOURCE_TYPES, - resourceTypeCompleter, - resourceIdForPromptCompleter, - resourceIdForResourceTemplateCompleter, - registerResourceTemplates -} from '../resources/templates.js'; -import { getSessionResourceURI, registerSessionResource } from '../resources/session.js'; -import { registerFileResources } from '../resources/files.js'; -import { setSubscriptionHandlers, beginSimulatedResourceUpdates, stopSimulatedResourceUpdates } from '../resources/subscriptions.js'; - -describe('Resource Templates', () => { - describe('Constants', () => { - it('should include both types in RESOURCE_TYPES array', () => { - expect(RESOURCE_TYPES).toContain(RESOURCE_TYPE_TEXT); - expect(RESOURCE_TYPES).toContain(RESOURCE_TYPE_BLOB); - expect(RESOURCE_TYPES).toHaveLength(2); - }); + textResource, + blobResource, + textResourceUri, + blobResourceUri, + RESOURCE_TYPE_TEXT, + RESOURCE_TYPE_BLOB, + RESOURCE_TYPES, + resourceTypeCompleter, + resourceIdForPromptCompleter, + resourceIdForResourceTemplateCompleter, + registerResourceTemplates, +} from "../resources/templates.js"; +import { + getSessionResourceURI, + registerSessionResource, +} from "../resources/session.js"; +import { registerFileResources } from "../resources/files.js"; +import { + setSubscriptionHandlers, + beginSimulatedResourceUpdates, + stopSimulatedResourceUpdates, +} from "../resources/subscriptions.js"; + +describe("Resource Templates", () => { + describe("Constants", () => { + it("should include both types in RESOURCE_TYPES array", () => { + expect(RESOURCE_TYPES).toContain(RESOURCE_TYPE_TEXT); + expect(RESOURCE_TYPES).toContain(RESOURCE_TYPE_BLOB); + expect(RESOURCE_TYPES).toHaveLength(2); }); + }); - describe('textResourceUri', () => { - it('should create URL for text resource', () => { - const uri = textResourceUri(1); - expect(uri.toString()).toBe('demo://resource/dynamic/text/1'); - }); + describe("textResourceUri", () => { + it("should create URL for text resource", () => { + const uri = textResourceUri(1); + expect(uri.toString()).toBe("demo://resource/dynamic/text/1"); + }); - it('should handle different resource IDs', () => { - expect(textResourceUri(5).toString()).toBe('demo://resource/dynamic/text/5'); - expect(textResourceUri(100).toString()).toBe('demo://resource/dynamic/text/100'); - }); + it("should handle different resource IDs", () => { + expect(textResourceUri(5).toString()).toBe( + "demo://resource/dynamic/text/5" + ); + expect(textResourceUri(100).toString()).toBe( + "demo://resource/dynamic/text/100" + ); }); + }); - describe('blobResourceUri', () => { - it('should create URL for blob resource', () => { - const uri = blobResourceUri(1); - expect(uri.toString()).toBe('demo://resource/dynamic/blob/1'); - }); + describe("blobResourceUri", () => { + it("should create URL for blob resource", () => { + const uri = blobResourceUri(1); + expect(uri.toString()).toBe("demo://resource/dynamic/blob/1"); + }); - it('should handle different resource IDs', () => { - expect(blobResourceUri(5).toString()).toBe('demo://resource/dynamic/blob/5'); - expect(blobResourceUri(100).toString()).toBe('demo://resource/dynamic/blob/100'); - }); + it("should handle different resource IDs", () => { + expect(blobResourceUri(5).toString()).toBe( + "demo://resource/dynamic/blob/5" + ); + expect(blobResourceUri(100).toString()).toBe( + "demo://resource/dynamic/blob/100" + ); }); + }); - describe('textResource', () => { - it('should create text resource with correct structure', () => { - const uri = textResourceUri(1); - const resource = textResource(uri, 1); + describe("textResource", () => { + it("should create text resource with correct structure", () => { + const uri = textResourceUri(1); + const resource = textResource(uri, 1); - expect(resource.uri).toBe(uri.toString()); - expect(resource.mimeType).toBe('text/plain'); - expect(resource.text).toContain('Resource 1'); - expect(resource.text).toContain('plaintext'); - }); + expect(resource.uri).toBe(uri.toString()); + expect(resource.mimeType).toBe("text/plain"); + expect(resource.text).toContain("Resource 1"); + expect(resource.text).toContain("plaintext"); + }); - it('should include timestamp in content', () => { - const uri = textResourceUri(2); - const resource = textResource(uri, 2); + it("should include timestamp in content", () => { + const uri = textResourceUri(2); + const resource = textResource(uri, 2); - // Timestamp format varies, just check it contains time-related content - expect(resource.text).toMatch(/\d/); - }); + // Timestamp format varies, just check it contains time-related content + expect(resource.text).toMatch(/\d/); }); + }); + + describe("blobResource", () => { + it("should create blob resource with correct structure", () => { + const uri = blobResourceUri(1); + const resource = blobResource(uri, 1); - describe('blobResource', () => { - it('should create blob resource with correct structure', () => { - const uri = blobResourceUri(1); - const resource = blobResource(uri, 1); - - expect(resource.uri).toBe(uri.toString()); - expect(resource.mimeType).toBe('text/plain'); - expect(resource.blob).toBeDefined(); - }); - - it('should create valid base64 encoded content', () => { - const uri = blobResourceUri(3); - const resource = blobResource(uri, 3); - - // Decode and verify content - const decoded = Buffer.from(resource.blob, 'base64').toString(); - expect(decoded).toContain('Resource 3'); - expect(decoded).toContain('base64 blob'); - }); + expect(resource.uri).toBe(uri.toString()); + expect(resource.mimeType).toBe("text/plain"); + expect(resource.blob).toBeDefined(); }); - describe('resourceTypeCompleter', () => { - it('should be defined as a completable schema', () => { - // The completer is a zod schema wrapped with completable - expect(resourceTypeCompleter).toBeDefined(); - // It should have the zod parse method - expect(typeof (resourceTypeCompleter as any).parse).toBe('function'); - }); - - it('should validate string resource types', () => { - // Test that valid strings pass validation - expect(() => (resourceTypeCompleter as any).parse('Text')).not.toThrow(); - expect(() => (resourceTypeCompleter as any).parse('Blob')).not.toThrow(); - }); + it("should create valid base64 encoded content", () => { + const uri = blobResourceUri(3); + const resource = blobResource(uri, 3); + + // Decode and verify content + const decoded = Buffer.from(resource.blob, "base64").toString(); + expect(decoded).toContain("Resource 3"); + expect(decoded).toContain("base64 blob"); + }); + }); + + describe("resourceTypeCompleter", () => { + it("should be defined as a completable schema", () => { + // The completer is a zod schema wrapped with completable + expect(resourceTypeCompleter).toBeDefined(); + // It should have the zod parse method + expect(typeof (resourceTypeCompleter as any).parse).toBe("function"); }); - describe('resourceIdForPromptCompleter', () => { - it('should be defined as a completable schema', () => { - expect(resourceIdForPromptCompleter).toBeDefined(); - expect(typeof (resourceIdForPromptCompleter as any).parse).toBe('function'); - }); - - it('should validate string IDs', () => { - // Test that valid strings pass validation - expect(() => (resourceIdForPromptCompleter as any).parse('1')).not.toThrow(); - expect(() => (resourceIdForPromptCompleter as any).parse('100')).not.toThrow(); - }); + it("should validate string resource types", () => { + // Test that valid strings pass validation + expect(() => (resourceTypeCompleter as any).parse("Text")).not.toThrow(); + expect(() => (resourceTypeCompleter as any).parse("Blob")).not.toThrow(); + }); + }); + + describe("resourceIdForPromptCompleter", () => { + it("should be defined as a completable schema", () => { + expect(resourceIdForPromptCompleter).toBeDefined(); + expect(typeof (resourceIdForPromptCompleter as any).parse).toBe( + "function" + ); }); - describe('resourceIdForResourceTemplateCompleter', () => { - it('should validate positive integer IDs', () => { - expect(resourceIdForResourceTemplateCompleter('1')).toEqual(['1']); - expect(resourceIdForResourceTemplateCompleter('50')).toEqual(['50']); - }); - - it('should reject invalid IDs', () => { - expect(resourceIdForResourceTemplateCompleter('0')).toEqual([]); - expect(resourceIdForResourceTemplateCompleter('-5')).toEqual([]); - expect(resourceIdForResourceTemplateCompleter('not-a-number')).toEqual([]); - }); + it("should validate string IDs", () => { + // Test that valid strings pass validation + expect(() => + (resourceIdForPromptCompleter as any).parse("1") + ).not.toThrow(); + expect(() => + (resourceIdForPromptCompleter as any).parse("100") + ).not.toThrow(); }); + }); - describe('registerResourceTemplates', () => { - it('should register text and blob resource templates', () => { - const registeredResources: any[] = []; + describe("resourceIdForResourceTemplateCompleter", () => { + it("should validate positive integer IDs", () => { + expect(resourceIdForResourceTemplateCompleter("1")).toEqual(["1"]); + expect(resourceIdForResourceTemplateCompleter("50")).toEqual(["50"]); + }); - const mockServer = { - registerResource: vi.fn((...args) => { - registeredResources.push(args); - }) - } as unknown as McpServer; + it("should reject invalid IDs", () => { + expect(resourceIdForResourceTemplateCompleter("0")).toEqual([]); + expect(resourceIdForResourceTemplateCompleter("-5")).toEqual([]); + expect(resourceIdForResourceTemplateCompleter("not-a-number")).toEqual( + [] + ); + }); + }); + + describe("registerResourceTemplates", () => { + it("should register text and blob resource templates", () => { + const registeredResources: any[] = []; + + const mockServer = { + registerResource: vi.fn((...args) => { + registeredResources.push(args); + }), + } as unknown as McpServer; + + registerResourceTemplates(mockServer); + + expect(mockServer.registerResource).toHaveBeenCalledTimes(2); + + // Check text resource registration + const textRegistration = registeredResources.find((r) => + r[0].includes("Text") + ); + expect(textRegistration).toBeDefined(); + expect(textRegistration[1]).toBeInstanceOf(ResourceTemplate); + + // Check blob resource registration + const blobRegistration = registeredResources.find((r) => + r[0].includes("Blob") + ); + expect(blobRegistration).toBeDefined(); + }); + }); +}); - registerResourceTemplates(mockServer); +describe("Session Resources", () => { + describe("getSessionResourceURI", () => { + it("should generate correct URI for resource name", () => { + expect(getSessionResourceURI("test")).toBe( + "demo://resource/session/test" + ); + }); - expect(mockServer.registerResource).toHaveBeenCalledTimes(2); + it("should handle various resource names", () => { + expect(getSessionResourceURI("my-file")).toBe( + "demo://resource/session/my-file" + ); + expect(getSessionResourceURI("document_123")).toBe( + "demo://resource/session/document_123" + ); + }); + }); + + describe("registerSessionResource", () => { + it("should register text resource and return resource link", () => { + const registrations: any[] = []; + const mockServer = { + registerResource: vi.fn((...args) => { + registrations.push(args); + }), + } as unknown as McpServer; + + const resource = { + uri: "demo://resource/session/test-file", + name: "test-file", + mimeType: "text/plain", + description: "A test file", + }; + + const result = registerSessionResource( + mockServer, + resource, + "text", + "Hello, World!" + ); + + expect(result.type).toBe("resource_link"); + expect(result.uri).toBe(resource.uri); + expect(result.name).toBe(resource.name); + + expect(mockServer.registerResource).toHaveBeenCalledWith( + "test-file", + "demo://resource/session/test-file", + expect.objectContaining({ + mimeType: "text/plain", + description: "A test file", + }), + expect.any(Function) + ); + }); - // Check text resource registration - const textRegistration = registeredResources.find(r => r[0].includes('Text')); - expect(textRegistration).toBeDefined(); - expect(textRegistration[1]).toBeInstanceOf(ResourceTemplate); + it("should register blob resource correctly", () => { + const mockServer = { + registerResource: vi.fn(), + } as unknown as McpServer; + + const resource = { + uri: "demo://resource/session/binary-file", + name: "binary-file", + mimeType: "application/octet-stream", + }; + + const blobContent = Buffer.from("binary data").toString("base64"); + const result = registerSessionResource( + mockServer, + resource, + "blob", + blobContent + ); + + expect(result.type).toBe("resource_link"); + expect(mockServer.registerResource).toHaveBeenCalled(); + }); - // Check blob resource registration - const blobRegistration = registeredResources.find(r => r[0].includes('Blob')); - expect(blobRegistration).toBeDefined(); - }); + it("should return resource handler that provides correct content", async () => { + let capturedHandler: Function | null = null; + const mockServer = { + registerResource: vi.fn((_name, _uri, _config, handler) => { + capturedHandler = handler; + }), + } as unknown as McpServer; + + const resource = { + uri: "demo://resource/session/content-test", + name: "content-test", + mimeType: "text/plain", + }; + + registerSessionResource( + mockServer, + resource, + "text", + "Test content here" + ); + + expect(capturedHandler).not.toBeNull(); + + const handlerResult = await capturedHandler!(new URL(resource.uri)); + expect(handlerResult.contents).toHaveLength(1); + expect(handlerResult.contents[0].text).toBe("Test content here"); + expect(handlerResult.contents[0].mimeType).toBe("text/plain"); }); + }); }); -describe('Session Resources', () => { - describe('getSessionResourceURI', () => { - it('should generate correct URI for resource name', () => { - expect(getSessionResourceURI('test')).toBe('demo://resource/session/test'); - }); +describe("File Resources", () => { + describe("registerFileResources", () => { + it("should register file resources when docs directory exists", () => { + const mockServer = { + registerResource: vi.fn(), + } as unknown as McpServer; - it('should handle various resource names', () => { - expect(getSessionResourceURI('my-file')).toBe('demo://resource/session/my-file'); - expect(getSessionResourceURI('document_123')).toBe('demo://resource/session/document_123'); - }); - }); + registerFileResources(mockServer); - describe('registerSessionResource', () => { - it('should register text resource and return resource link', () => { - const registrations: any[] = []; - const mockServer = { - registerResource: vi.fn((...args) => { - registrations.push(args); - }) - } as unknown as McpServer; - - const resource = { - uri: 'demo://resource/session/test-file', - name: 'test-file', - mimeType: 'text/plain', - description: 'A test file' - }; - - const result = registerSessionResource(mockServer, resource, 'text', 'Hello, World!'); - - expect(result.type).toBe('resource_link'); - expect(result.uri).toBe(resource.uri); - expect(result.name).toBe(resource.name); - - expect(mockServer.registerResource).toHaveBeenCalledWith( - 'test-file', - 'demo://resource/session/test-file', - expect.objectContaining({ - mimeType: 'text/plain', - description: 'A test file' - }), - expect.any(Function) - ); - }); - - it('should register blob resource correctly', () => { - const mockServer = { - registerResource: vi.fn() - } as unknown as McpServer; - - const resource = { - uri: 'demo://resource/session/binary-file', - name: 'binary-file', - mimeType: 'application/octet-stream' - }; - - const blobContent = Buffer.from('binary data').toString('base64'); - const result = registerSessionResource(mockServer, resource, 'blob', blobContent); - - expect(result.type).toBe('resource_link'); - expect(mockServer.registerResource).toHaveBeenCalled(); - }); - - it('should return resource handler that provides correct content', async () => { - let capturedHandler: Function | null = null; - const mockServer = { - registerResource: vi.fn((_name, _uri, _config, handler) => { - capturedHandler = handler; - }) - } as unknown as McpServer; - - const resource = { - uri: 'demo://resource/session/content-test', - name: 'content-test', - mimeType: 'text/plain' - }; - - registerSessionResource(mockServer, resource, 'text', 'Test content here'); - - expect(capturedHandler).not.toBeNull(); - - const handlerResult = await capturedHandler!(new URL(resource.uri)); - expect(handlerResult.contents).toHaveLength(1); - expect(handlerResult.contents[0].text).toBe('Test content here'); - expect(handlerResult.contents[0].mimeType).toBe('text/plain'); - }); + // The docs folder exists in the everything server and contains files + // so registerResource should have been called + expect(mockServer.registerResource).toHaveBeenCalled(); }); + }); }); -describe('File Resources', () => { - describe('registerFileResources', () => { - it('should register file resources when docs directory exists', () => { - const mockServer = { - registerResource: vi.fn() - } as unknown as McpServer; +describe("Subscriptions", () => { + describe("setSubscriptionHandlers", () => { + it("should set request handlers on server", () => { + const mockServer = { + server: { + setRequestHandler: vi.fn(), + }, + sendLoggingMessage: vi.fn(), + } as unknown as McpServer; - registerFileResources(mockServer); + setSubscriptionHandlers(mockServer); - // The docs folder exists in the everything server and contains files - // so registerResource should have been called - expect(mockServer.registerResource).toHaveBeenCalled(); - }); + // Should set both subscribe and unsubscribe handlers + expect(mockServer.server.setRequestHandler).toHaveBeenCalledTimes(2); }); -}); + }); -describe('Subscriptions', () => { - describe('setSubscriptionHandlers', () => { - it('should set request handlers on server', () => { - const mockServer = { - server: { - setRequestHandler: vi.fn() - }, - sendLoggingMessage: vi.fn() - } as unknown as McpServer; - - setSubscriptionHandlers(mockServer); - - // Should set both subscribe and unsubscribe handlers - expect(mockServer.server.setRequestHandler).toHaveBeenCalledTimes(2); - }); + describe("simulated resource updates lifecycle", () => { + afterEach(() => { + // Clean up any intervals + stopSimulatedResourceUpdates("lifecycle-test-session"); }); - describe('simulated resource updates lifecycle', () => { - afterEach(() => { - // Clean up any intervals - stopSimulatedResourceUpdates('lifecycle-test-session'); - }); - - it('should start and stop updates without errors', () => { - const mockServer = { - server: { - notification: vi.fn() - } - } as unknown as McpServer; - - // Start updates - should work for both defined and undefined sessionId - beginSimulatedResourceUpdates(mockServer, 'lifecycle-test-session'); - beginSimulatedResourceUpdates(mockServer, undefined); - - // Stop updates - should handle all cases gracefully - stopSimulatedResourceUpdates('lifecycle-test-session'); - stopSimulatedResourceUpdates('non-existent-session'); - stopSimulatedResourceUpdates(undefined); - - // If we got here without throwing, the lifecycle works correctly - expect(true).toBe(true); - }); + it("should start and stop updates without errors", () => { + const mockServer = { + server: { + notification: vi.fn(), + }, + } as unknown as McpServer; + + // Start updates - should work for both defined and undefined sessionId + beginSimulatedResourceUpdates(mockServer, "lifecycle-test-session"); + beginSimulatedResourceUpdates(mockServer, undefined); + + // Stop updates - should handle all cases gracefully + stopSimulatedResourceUpdates("lifecycle-test-session"); + stopSimulatedResourceUpdates("non-existent-session"); + stopSimulatedResourceUpdates(undefined); + + // If we got here without throwing, the lifecycle works correctly + expect(true).toBe(true); }); + }); }); diff --git a/src/everything/__tests__/server.test.ts b/src/everything/__tests__/server.test.ts index 15ad7640ef..c7010a0627 100644 --- a/src/everything/__tests__/server.test.ts +++ b/src/everything/__tests__/server.test.ts @@ -1,41 +1,41 @@ -import { describe, it, expect, vi } from 'vitest'; -import { createServer } from '../server/index.js'; +import { describe, it, expect, vi } from "vitest"; +import { createServer } from "../server/index.js"; -describe('Server Factory', () => { - describe('createServer', () => { - it('should return a ServerFactoryResponse object', () => { - const result = createServer(); +describe("Server Factory", () => { + describe("createServer", () => { + it("should return a ServerFactoryResponse object", () => { + const result = createServer(); - expect(result).toHaveProperty('server'); - expect(result).toHaveProperty('cleanup'); - }); + expect(result).toHaveProperty("server"); + expect(result).toHaveProperty("cleanup"); + }); - it('should return a cleanup function', () => { - const { cleanup } = createServer(); + it("should return a cleanup function", () => { + const { cleanup } = createServer(); - expect(typeof cleanup).toBe('function'); - }); + expect(typeof cleanup).toBe("function"); + }); - it('should create an McpServer instance', () => { - const { server } = createServer(); + it("should create an McpServer instance", () => { + const { server } = createServer(); - expect(server).toBeDefined(); - expect(server.server).toBeDefined(); - }); + expect(server).toBeDefined(); + expect(server.server).toBeDefined(); + }); - it('should have an oninitialized handler set', () => { - const { server } = createServer(); + it("should have an oninitialized handler set", () => { + const { server } = createServer(); - expect(server.server.oninitialized).toBeDefined(); - }); + expect(server.server.oninitialized).toBeDefined(); + }); - it('should allow multiple servers to be created', () => { - const result1 = createServer(); - const result2 = createServer(); + it("should allow multiple servers to be created", () => { + const result1 = createServer(); + const result2 = createServer(); - expect(result1.server).toBeDefined(); - expect(result2.server).toBeDefined(); - expect(result1.server).not.toBe(result2.server); - }); + expect(result1.server).toBeDefined(); + expect(result2.server).toBeDefined(); + expect(result1.server).not.toBe(result2.server); }); + }); }); diff --git a/src/everything/__tests__/tools.test.ts b/src/everything/__tests__/tools.test.ts index 4ab313962a..85314c8bbe 100644 --- a/src/everything/__tests__/tools.test.ts +++ b/src/everything/__tests__/tools.test.ts @@ -1,1101 +1,1175 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { McpServer } from '@modelcontextprotocol/server'; -import { registerEchoTool, EchoSchema } from '../tools/echo.js'; -import { registerGetSumTool } from '../tools/get-sum.js'; -import { registerGetEnvTool } from '../tools/get-env.js'; -import { registerGetTinyImageTool, MCP_TINY_IMAGE } from '../tools/get-tiny-image.js'; -import { registerGetStructuredContentTool } from '../tools/get-structured-content.js'; -import { registerGetAnnotatedMessageTool } from '../tools/get-annotated-message.js'; -import { registerTriggerLongRunningOperationTool } from '../tools/trigger-long-running-operation.js'; -import { registerGetResourceLinksTool } from '../tools/get-resource-links.js'; -import { registerGetResourceReferenceTool } from '../tools/get-resource-reference.js'; -import { registerToggleSimulatedLoggingTool } from '../tools/toggle-simulated-logging.js'; -import { registerToggleSubscriberUpdatesTool } from '../tools/toggle-subscriber-updates.js'; -import { registerTriggerSamplingRequestTool } from '../tools/trigger-sampling-request.js'; -import { registerTriggerElicitationRequestTool } from '../tools/trigger-elicitation-request.js'; -import { registerTriggerUrlElicitationTool, __resetIssuedErrorPathElicitations } from '../tools/trigger-url-elicitation.js'; -import { registerGetRootsListTool } from '../tools/get-roots-list.js'; -import { registerGZipFileAsResourceTool } from '../tools/gzip-file-as-resource.js'; +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { McpServer } from "@modelcontextprotocol/server"; +import { registerEchoTool, EchoSchema } from "../tools/echo.js"; +import { registerGetSumTool } from "../tools/get-sum.js"; +import { registerGetEnvTool } from "../tools/get-env.js"; +import { + registerGetTinyImageTool, + MCP_TINY_IMAGE, +} from "../tools/get-tiny-image.js"; +import { registerGetStructuredContentTool } from "../tools/get-structured-content.js"; +import { registerGetAnnotatedMessageTool } from "../tools/get-annotated-message.js"; +import { registerTriggerLongRunningOperationTool } from "../tools/trigger-long-running-operation.js"; +import { registerGetResourceLinksTool } from "../tools/get-resource-links.js"; +import { registerGetResourceReferenceTool } from "../tools/get-resource-reference.js"; +import { registerToggleSimulatedLoggingTool } from "../tools/toggle-simulated-logging.js"; +import { registerToggleSubscriberUpdatesTool } from "../tools/toggle-subscriber-updates.js"; +import { registerTriggerSamplingRequestTool } from "../tools/trigger-sampling-request.js"; +import { registerTriggerElicitationRequestTool } from "../tools/trigger-elicitation-request.js"; +import { + registerTriggerUrlElicitationTool, + __resetIssuedErrorPathElicitations, +} from "../tools/trigger-url-elicitation.js"; +import { registerGetRootsListTool } from "../tools/get-roots-list.js"; +import { registerGZipFileAsResourceTool } from "../tools/gzip-file-as-resource.js"; // Helper to capture registered tool handlers function createMockServer() { - const handlers: Map = new Map(); - const configs: Map = new Map(); + const handlers: Map = new Map(); + const configs: Map = new Map(); + + const mockServer = { + registerTool: vi.fn((name: string, config: any, handler: Function) => { + handlers.set(name, handler); + configs.set(name, config); + }), + server: { + getClientCapabilities: vi.fn(() => ({})), + notification: vi.fn(), + }, + sendLoggingMessage: vi.fn(), + sendResourceUpdated: vi.fn(), + } as unknown as McpServer; + + return { mockServer, handlers, configs }; +} - const mockServer = { - registerTool: vi.fn((name: string, config: any, handler: Function) => { - handlers.set(name, handler); - configs.set(name, config); +describe("Tools", () => { + describe("echo", () => { + it("should echo back the message", async () => { + const { mockServer, handlers } = createMockServer(); + registerEchoTool(mockServer); + + const handler = handlers.get("echo")!; + const result = await handler({ message: "Hello, World!" }); + + expect(result).toEqual({ + content: [{ type: "text", text: "Echo: Hello, World!" }], + }); + }); + + it("should handle empty message", async () => { + const { mockServer, handlers } = createMockServer(); + registerEchoTool(mockServer); + + const handler = handlers.get("echo")!; + const result = await handler({ message: "" }); + + expect(result).toEqual({ + content: [{ type: "text", text: "Echo: " }], + }); + }); + + it("should reject invalid input", async () => { + const { mockServer, handlers } = createMockServer(); + registerEchoTool(mockServer); + + const handler = handlers.get("echo")!; + + await expect(handler({})).rejects.toThrow(); + await expect(handler({ message: 123 })).rejects.toThrow(); + }); + }); + + describe("EchoSchema", () => { + it("should validate correct input", () => { + const result = EchoSchema.parse({ message: "test" }); + expect(result).toEqual({ message: "test" }); + }); + + it("should reject missing message", () => { + expect(() => EchoSchema.parse({})).toThrow(); + }); + + it("should reject non-string message", () => { + expect(() => EchoSchema.parse({ message: 123 })).toThrow(); + }); + }); + + describe("get-sum", () => { + it("should calculate sum of two positive numbers", async () => { + const { mockServer, handlers } = createMockServer(); + registerGetSumTool(mockServer); + + const handler = handlers.get("get-sum")!; + const result = await handler({ a: 5, b: 3 }); + + expect(result).toEqual({ + content: [{ type: "text", text: "The sum of 5 and 3 is 8." }], + }); + }); + + it("should calculate sum with negative numbers", async () => { + const { mockServer, handlers } = createMockServer(); + registerGetSumTool(mockServer); + + const handler = handlers.get("get-sum")!; + const result = await handler({ a: -5, b: 3 }); + + expect(result).toEqual({ + content: [{ type: "text", text: "The sum of -5 and 3 is -2." }], + }); + }); + + it("should calculate sum with zero", async () => { + const { mockServer, handlers } = createMockServer(); + registerGetSumTool(mockServer); + + const handler = handlers.get("get-sum")!; + const result = await handler({ a: 0, b: 0 }); + + expect(result).toEqual({ + content: [{ type: "text", text: "The sum of 0 and 0 is 0." }], + }); + }); + + it("should handle floating point numbers", async () => { + const { mockServer, handlers } = createMockServer(); + registerGetSumTool(mockServer); + + const handler = handlers.get("get-sum")!; + const result = await handler({ a: 1.5, b: 2.5 }); + + expect(result).toEqual({ + content: [{ type: "text", text: "The sum of 1.5 and 2.5 is 4." }], + }); + }); + + it("should reject invalid input", async () => { + const { mockServer, handlers } = createMockServer(); + registerGetSumTool(mockServer); + + const handler = handlers.get("get-sum")!; + + await expect(handler({})).rejects.toThrow(); + await expect(handler({ a: "not a number", b: 5 })).rejects.toThrow(); + await expect(handler({ a: 5 })).rejects.toThrow(); + }); + }); + + describe("get-env", () => { + it("should return all environment variables as JSON", async () => { + const { mockServer, handlers } = createMockServer(); + registerGetEnvTool(mockServer); + + const handler = handlers.get("get-env")!; + process.env.TEST_VAR_EVERYTHING = "test_value"; + const result = await handler({}); + + expect(result.content).toHaveLength(1); + expect(result.content[0].type).toBe("text"); + + const envJson = JSON.parse(result.content[0].text); + expect(envJson.TEST_VAR_EVERYTHING).toBe("test_value"); + + delete process.env.TEST_VAR_EVERYTHING; + }); + + it("should return valid JSON", async () => { + const { mockServer, handlers } = createMockServer(); + registerGetEnvTool(mockServer); + + const handler = handlers.get("get-env")!; + const result = await handler({}); + + expect(() => JSON.parse(result.content[0].text)).not.toThrow(); + }); + }); + + describe("get-tiny-image", () => { + it("should return image content with text descriptions", async () => { + const { mockServer, handlers } = createMockServer(); + registerGetTinyImageTool(mockServer); + + const handler = handlers.get("get-tiny-image")!; + const result = await handler({}); + + expect(result.content).toHaveLength(3); + expect(result.content[0]).toEqual({ + type: "text", + text: "Here's the image you requested:", + }); + expect(result.content[1]).toEqual({ + type: "image", + data: MCP_TINY_IMAGE, + mimeType: "image/png", + }); + expect(result.content[2]).toEqual({ + type: "text", + text: "The image above is the MCP logo.", + }); + }); + + it("should return valid base64 image data", async () => { + const { mockServer, handlers } = createMockServer(); + registerGetTinyImageTool(mockServer); + + const handler = handlers.get("get-tiny-image")!; + const result = await handler({}); + + const imageContent = result.content[1]; + expect(imageContent.type).toBe("image"); + expect(imageContent.mimeType).toBe("image/png"); + // Verify it's valid base64 + expect(() => Buffer.from(imageContent.data, "base64")).not.toThrow(); + }); + }); + + describe("get-structured-content", () => { + it("should return weather for New York", async () => { + const { mockServer, handlers } = createMockServer(); + registerGetStructuredContentTool(mockServer); + + const handler = handlers.get("get-structured-content")!; + const result = await handler({ location: "New York" }); + + expect(result.structuredContent).toEqual({ + temperature: 33, + conditions: "Cloudy", + humidity: 82, + }); + expect(result.content[0].type).toBe("text"); + expect(JSON.parse(result.content[0].text)).toEqual( + result.structuredContent + ); + }); + + it("should return weather for Chicago", async () => { + const { mockServer, handlers } = createMockServer(); + registerGetStructuredContentTool(mockServer); + + const handler = handlers.get("get-structured-content")!; + const result = await handler({ location: "Chicago" }); + + expect(result.structuredContent).toEqual({ + temperature: 36, + conditions: "Light rain / drizzle", + humidity: 82, + }); + }); + + it("should return weather for Los Angeles", async () => { + const { mockServer, handlers } = createMockServer(); + registerGetStructuredContentTool(mockServer); + + const handler = handlers.get("get-structured-content")!; + const result = await handler({ location: "Los Angeles" }); + + expect(result.structuredContent).toEqual({ + temperature: 73, + conditions: "Sunny / Clear", + humidity: 48, + }); + }); + }); + + describe("get-annotated-message", () => { + it("should return error message with high priority", async () => { + const { mockServer, handlers } = createMockServer(); + registerGetAnnotatedMessageTool(mockServer); + + const handler = handlers.get("get-annotated-message")!; + const result = await handler({ + messageType: "error", + includeImage: false, + }); + + expect(result.content).toHaveLength(1); + expect(result.content[0].text).toBe("Error: Operation failed"); + expect(result.content[0].annotations).toEqual({ + priority: 1.0, + audience: ["user", "assistant"], + }); + }); + + it("should return success message with medium priority", async () => { + const { mockServer, handlers } = createMockServer(); + registerGetAnnotatedMessageTool(mockServer); + + const handler = handlers.get("get-annotated-message")!; + const result = await handler({ + messageType: "success", + includeImage: false, + }); + + expect(result.content[0].text).toBe("Operation completed successfully"); + expect(result.content[0].annotations.priority).toBe(0.7); + expect(result.content[0].annotations.audience).toEqual(["user"]); + }); + + it("should return debug message with low priority", async () => { + const { mockServer, handlers } = createMockServer(); + registerGetAnnotatedMessageTool(mockServer); + + const handler = handlers.get("get-annotated-message")!; + const result = await handler({ + messageType: "debug", + includeImage: false, + }); + + expect(result.content[0].text).toContain("Debug:"); + expect(result.content[0].annotations.priority).toBe(0.3); + expect(result.content[0].annotations.audience).toEqual(["assistant"]); + }); + + it("should include annotated image when requested", async () => { + const { mockServer, handlers } = createMockServer(); + registerGetAnnotatedMessageTool(mockServer); + + const handler = handlers.get("get-annotated-message")!; + const result = await handler({ + messageType: "success", + includeImage: true, + }); + + expect(result.content).toHaveLength(2); + expect(result.content[1].type).toBe("image"); + expect(result.content[1].annotations).toEqual({ + priority: 0.5, + audience: ["user"], + }); + }); + }); + + describe("trigger-long-running-operation", () => { + it("should complete operation and return result", async () => { + const { mockServer, handlers } = createMockServer(); + registerTriggerLongRunningOperationTool(mockServer); + + const handler = handlers.get("trigger-long-running-operation")!; + // Use very short duration for test + const result = await handler( + { duration: 0.1, steps: 2 }, + { mcpReq: { _meta: {}, id: "test-123" } } + ); + + expect(result.content[0].text).toContain( + "Long running operation completed" + ); + expect(result.content[0].text).toContain("Duration: 0.1 seconds"); + expect(result.content[0].text).toContain("Steps: 2"); + }, 10000); + + it("should send progress notifications when progressToken provided", async () => { + const { mockServer, handlers } = createMockServer(); + registerTriggerLongRunningOperationTool(mockServer); + + const handler = handlers.get("trigger-long-running-operation")!; + await handler( + { duration: 0.1, steps: 2 }, + { + mcpReq: { _meta: { progressToken: "token-123" }, id: "test-456" }, + sessionId: "session-1", + } + ); + + expect(mockServer.server.notification).toHaveBeenCalledTimes(2); + expect(mockServer.server.notification).toHaveBeenCalledWith( + expect.objectContaining({ + method: "notifications/progress", + params: expect.objectContaining({ + progressToken: "token-123", + }), }), - server: { - getClientCapabilities: vi.fn(() => ({})), - notification: vi.fn() - }, - sendLoggingMessage: vi.fn(), - sendResourceUpdated: vi.fn() - } as unknown as McpServer; + expect.any(Object) + ); + }, 10000); + }); + + describe("get-resource-links", () => { + it("should return specified number of resource links", async () => { + const { mockServer, handlers } = createMockServer(); + registerGetResourceLinksTool(mockServer); + + const handler = handlers.get("get-resource-links")!; + const result = await handler({ count: 3 }); + + // 1 intro text + 3 resource links + expect(result.content).toHaveLength(4); + expect(result.content[0].type).toBe("text"); + expect(result.content[0].text).toContain("3 resource links"); + + // Check resource links + for (let i = 1; i < 4; i++) { + expect(result.content[i].type).toBe("resource_link"); + expect(result.content[i].uri).toBeDefined(); + expect(result.content[i].name).toBeDefined(); + } + }); - return { mockServer, handlers, configs }; -} + it("should alternate between text and blob resources", async () => { + const { mockServer, handlers } = createMockServer(); + registerGetResourceLinksTool(mockServer); -describe('Tools', () => { - describe('echo', () => { - it('should echo back the message', async () => { - const { mockServer, handlers } = createMockServer(); - registerEchoTool(mockServer); + const handler = handlers.get("get-resource-links")!; + const result = await handler({ count: 4 }); - const handler = handlers.get('echo')!; - const result = await handler({ message: 'Hello, World!' }); + // Odd IDs (1, 3) are blob, even IDs (2, 4) are text + expect(result.content[1].name).toContain("Blob"); + expect(result.content[2].name).toContain("Text"); + expect(result.content[3].name).toContain("Blob"); + expect(result.content[4].name).toContain("Text"); + }); - expect(result).toEqual({ - content: [{ type: 'text', text: 'Echo: Hello, World!' }] - }); - }); + it("should use default count of 3", async () => { + const { mockServer, handlers } = createMockServer(); + registerGetResourceLinksTool(mockServer); - it('should handle empty message', async () => { - const { mockServer, handlers } = createMockServer(); - registerEchoTool(mockServer); + const handler = handlers.get("get-resource-links")!; + const result = await handler({}); - const handler = handlers.get('echo')!; - const result = await handler({ message: '' }); + // 1 intro text + 3 resource links (default) + expect(result.content).toHaveLength(4); + }); + }); - expect(result).toEqual({ - content: [{ type: 'text', text: 'Echo: ' }] - }); - }); + describe("get-resource-reference", () => { + it("should return text resource reference", async () => { + const { mockServer, handlers } = createMockServer(); + registerGetResourceReferenceTool(mockServer); + + const handler = handlers.get("get-resource-reference")!; + const result = await handler({ resourceType: "Text", resourceId: 1 }); + + expect(result.content).toHaveLength(3); + expect(result.content[0].text).toContain("Resource 1"); + expect(result.content[1].type).toBe("resource"); + expect(result.content[1].resource.uri).toContain("text/1"); + expect(result.content[2].text).toContain("URI"); + }); - it('should reject invalid input', async () => { - const { mockServer, handlers } = createMockServer(); - registerEchoTool(mockServer); + it("should return blob resource reference", async () => { + const { mockServer, handlers } = createMockServer(); + registerGetResourceReferenceTool(mockServer); - const handler = handlers.get('echo')!; + const handler = handlers.get("get-resource-reference")!; + const result = await handler({ resourceType: "Blob", resourceId: 5 }); - await expect(handler({})).rejects.toThrow(); - await expect(handler({ message: 123 })).rejects.toThrow(); - }); + expect(result.content[1].resource.uri).toContain("blob/5"); }); - describe('EchoSchema', () => { - it('should validate correct input', () => { - const result = EchoSchema.parse({ message: 'test' }); - expect(result).toEqual({ message: 'test' }); - }); + it("should reject invalid resource type", async () => { + const { mockServer, handlers } = createMockServer(); + registerGetResourceReferenceTool(mockServer); - it('should reject missing message', () => { - expect(() => EchoSchema.parse({})).toThrow(); - }); + const handler = handlers.get("get-resource-reference")!; + await expect( + handler({ resourceType: "Invalid", resourceId: 1 }) + ).rejects.toThrow("Invalid resourceType"); + }); - it('should reject non-string message', () => { - expect(() => EchoSchema.parse({ message: 123 })).toThrow(); - }); + it("should reject invalid resource ID", async () => { + const { mockServer, handlers } = createMockServer(); + registerGetResourceReferenceTool(mockServer); + + const handler = handlers.get("get-resource-reference")!; + await expect( + handler({ resourceType: "Text", resourceId: -1 }) + ).rejects.toThrow("Invalid resourceId"); + await expect( + handler({ resourceType: "Text", resourceId: 0 }) + ).rejects.toThrow("Invalid resourceId"); + await expect( + handler({ resourceType: "Text", resourceId: 1.5 }) + ).rejects.toThrow("Invalid resourceId"); }); + }); + + describe("toggle-simulated-logging", () => { + it("should start logging when not active", async () => { + const { mockServer, handlers } = createMockServer(); + registerToggleSimulatedLoggingTool(mockServer); - describe('get-sum', () => { - it('should calculate sum of two positive numbers', async () => { - const { mockServer, handlers } = createMockServer(); - registerGetSumTool(mockServer); + const handler = handlers.get("toggle-simulated-logging")!; + const result = await handler({}, { sessionId: "test-session-1" }); - const handler = handlers.get('get-sum')!; - const result = await handler({ a: 5, b: 3 }); + expect(result.content[0].text).toContain("Started"); + expect(result.content[0].text).toContain("test-session-1"); + }); - expect(result).toEqual({ - content: [{ type: 'text', text: 'The sum of 5 and 3 is 8.' }] - }); - }); + it("should stop logging when already active", async () => { + const { mockServer, handlers } = createMockServer(); + registerToggleSimulatedLoggingTool(mockServer); - it('should calculate sum with negative numbers', async () => { - const { mockServer, handlers } = createMockServer(); - registerGetSumTool(mockServer); + const handler = handlers.get("toggle-simulated-logging")!; - const handler = handlers.get('get-sum')!; - const result = await handler({ a: -5, b: 3 }); + // First call starts logging + await handler({}, { sessionId: "test-session-2" }); - expect(result).toEqual({ - content: [{ type: 'text', text: 'The sum of -5 and 3 is -2.' }] - }); - }); + // Second call stops logging + const result = await handler({}, { sessionId: "test-session-2" }); - it('should calculate sum with zero', async () => { - const { mockServer, handlers } = createMockServer(); - registerGetSumTool(mockServer); + expect(result.content[0].text).toContain("Stopped"); + expect(result.content[0].text).toContain("test-session-2"); + }); - const handler = handlers.get('get-sum')!; - const result = await handler({ a: 0, b: 0 }); + it("should handle undefined sessionId", async () => { + const { mockServer, handlers } = createMockServer(); + registerToggleSimulatedLoggingTool(mockServer); - expect(result).toEqual({ - content: [{ type: 'text', text: 'The sum of 0 and 0 is 0.' }] - }); - }); + const handler = handlers.get("toggle-simulated-logging")!; + const result = await handler({}, {}); - it('should handle floating point numbers', async () => { - const { mockServer, handlers } = createMockServer(); - registerGetSumTool(mockServer); + expect(result.content[0].text).toContain("Started"); + }); + }); - const handler = handlers.get('get-sum')!; - const result = await handler({ a: 1.5, b: 2.5 }); + describe("toggle-subscriber-updates", () => { + it("should start updates when not active", async () => { + const { mockServer, handlers } = createMockServer(); + registerToggleSubscriberUpdatesTool(mockServer); - expect(result).toEqual({ - content: [{ type: 'text', text: 'The sum of 1.5 and 2.5 is 4.' }] - }); - }); + const handler = handlers.get("toggle-subscriber-updates")!; + const result = await handler({}, { sessionId: "sub-session-1" }); - it('should reject invalid input', async () => { - const { mockServer, handlers } = createMockServer(); - registerGetSumTool(mockServer); + expect(result.content[0].text).toContain("Started"); + expect(result.content[0].text).toContain("sub-session-1"); + }); - const handler = handlers.get('get-sum')!; + it("should stop updates when already active", async () => { + const { mockServer, handlers } = createMockServer(); + registerToggleSubscriberUpdatesTool(mockServer); - await expect(handler({})).rejects.toThrow(); - await expect(handler({ a: 'not a number', b: 5 })).rejects.toThrow(); - await expect(handler({ a: 5 })).rejects.toThrow(); - }); + const handler = handlers.get("toggle-subscriber-updates")!; + + // First call starts updates + await handler({}, { sessionId: "sub-session-2" }); + + // Second call stops updates + const result = await handler({}, { sessionId: "sub-session-2" }); + + expect(result.content[0].text).toContain("Stopped"); + expect(result.content[0].text).toContain("sub-session-2"); }); + }); - describe('get-env', () => { - it('should return all environment variables as JSON', async () => { - const { mockServer, handlers } = createMockServer(); - registerGetEnvTool(mockServer); + describe("trigger-sampling-request", () => { + it("should not register when client does not support sampling", () => { + const { mockServer } = createMockServer(); + registerTriggerSamplingRequestTool(mockServer); - const handler = handlers.get('get-env')!; - process.env.TEST_VAR_EVERYTHING = 'test_value'; - const result = await handler({}); + // Tool should not be registered since mock server returns empty capabilities + expect(mockServer.registerTool).not.toHaveBeenCalled(); + }); + + it("should register when client supports sampling", () => { + const handlers: Map = new Map(); + const mockServer = { + registerTool: vi.fn((name: string, config: any, handler: Function) => { + handlers.set(name, handler); + }), + server: { + getClientCapabilities: vi.fn(() => ({ sampling: {} })), + }, + } as unknown as McpServer; - expect(result.content).toHaveLength(1); - expect(result.content[0].type).toBe('text'); + registerTriggerSamplingRequestTool(mockServer); - const envJson = JSON.parse(result.content[0].text); - expect(envJson.TEST_VAR_EVERYTHING).toBe('test_value'); + expect(mockServer.registerTool).toHaveBeenCalledWith( + "trigger-sampling-request", + expect.objectContaining({ + title: "Trigger Sampling Request Tool", + description: expect.stringContaining("Sampling"), + }), + expect.any(Function) + ); + }); - delete process.env.TEST_VAR_EVERYTHING; - }); + it("should send sampling request and return result", async () => { + const handlers: Map = new Map(); + const mockSendRequest = vi.fn().mockResolvedValue({ + model: "test-model", + content: { type: "text", text: "LLM response" }, + }); - it('should return valid JSON', async () => { - const { mockServer, handlers } = createMockServer(); - registerGetEnvTool(mockServer); + const mockServer = { + registerTool: vi.fn((name: string, config: any, handler: Function) => { + handlers.set(name, handler); + }), + server: { + getClientCapabilities: vi.fn(() => ({ sampling: {} })), + }, + } as unknown as McpServer; + + registerTriggerSamplingRequestTool(mockServer); + + const handler = handlers.get("trigger-sampling-request")!; + const result = await handler( + { prompt: "Test prompt", maxTokens: 50 }, + { mcpReq: { send: mockSendRequest } } + ); + + expect(mockSendRequest).toHaveBeenCalledWith( + expect.objectContaining({ + method: "sampling/createMessage", + params: expect.objectContaining({ + maxTokens: 50, + }), + }), + expect.anything() + ); + expect(result.content[0].text).toContain("LLM sampling result"); + }); + }); - const handler = handlers.get('get-env')!; - const result = await handler({}); + describe("trigger-elicitation-request", () => { + it("should not register when client does not support elicitation", () => { + const { mockServer } = createMockServer(); + registerTriggerElicitationRequestTool(mockServer); - expect(() => JSON.parse(result.content[0].text)).not.toThrow(); - }); + expect(mockServer.registerTool).not.toHaveBeenCalled(); }); - describe('get-tiny-image', () => { - it('should return image content with text descriptions', async () => { - const { mockServer, handlers } = createMockServer(); - registerGetTinyImageTool(mockServer); - - const handler = handlers.get('get-tiny-image')!; - const result = await handler({}); - - expect(result.content).toHaveLength(3); - expect(result.content[0]).toEqual({ - type: 'text', - text: "Here's the image you requested:" - }); - expect(result.content[1]).toEqual({ - type: 'image', - data: MCP_TINY_IMAGE, - mimeType: 'image/png' - }); - expect(result.content[2]).toEqual({ - type: 'text', - text: 'The image above is the MCP logo.' - }); - }); - - it('should return valid base64 image data', async () => { - const { mockServer, handlers } = createMockServer(); - registerGetTinyImageTool(mockServer); - - const handler = handlers.get('get-tiny-image')!; - const result = await handler({}); - - const imageContent = result.content[1]; - expect(imageContent.type).toBe('image'); - expect(imageContent.mimeType).toBe('image/png'); - // Verify it's valid base64 - expect(() => Buffer.from(imageContent.data, 'base64')).not.toThrow(); - }); + it("should register when client supports elicitation", () => { + const handlers: Map = new Map(); + const mockServer = { + registerTool: vi.fn((name: string, config: any, handler: Function) => { + handlers.set(name, handler); + }), + server: { + getClientCapabilities: vi.fn(() => ({ elicitation: {} })), + }, + } as unknown as McpServer; + + registerTriggerElicitationRequestTool(mockServer); + + expect(mockServer.registerTool).toHaveBeenCalledWith( + "trigger-elicitation-request", + expect.objectContaining({ + title: "Trigger Elicitation Request Tool", + description: expect.stringContaining("Elicitation"), + }), + expect.any(Function) + ); }); - describe('get-structured-content', () => { - it('should return weather for New York', async () => { - const { mockServer, handlers } = createMockServer(); - registerGetStructuredContentTool(mockServer); - - const handler = handlers.get('get-structured-content')!; - const result = await handler({ location: 'New York' }); - - expect(result.structuredContent).toEqual({ - temperature: 33, - conditions: 'Cloudy', - humidity: 82 - }); - expect(result.content[0].type).toBe('text'); - expect(JSON.parse(result.content[0].text)).toEqual(result.structuredContent); - }); - - it('should return weather for Chicago', async () => { - const { mockServer, handlers } = createMockServer(); - registerGetStructuredContentTool(mockServer); - - const handler = handlers.get('get-structured-content')!; - const result = await handler({ location: 'Chicago' }); - - expect(result.structuredContent).toEqual({ - temperature: 36, - conditions: 'Light rain / drizzle', - humidity: 82 - }); - }); - - it('should return weather for Los Angeles', async () => { - const { mockServer, handlers } = createMockServer(); - registerGetStructuredContentTool(mockServer); - - const handler = handlers.get('get-structured-content')!; - const result = await handler({ location: 'Los Angeles' }); - - expect(result.structuredContent).toEqual({ - temperature: 73, - conditions: 'Sunny / Clear', - humidity: 48 - }); - }); + it("should handle accept action with user content", async () => { + const handlers: Map = new Map(); + const mockSendRequest = vi.fn().mockResolvedValue({ + action: "accept", + content: { + name: "John Doe", + check: true, + email: "john@example.com", + }, + }); + + const mockServer = { + registerTool: vi.fn((name: string, config: any, handler: Function) => { + handlers.set(name, handler); + }), + server: { + getClientCapabilities: vi.fn(() => ({ elicitation: {} })), + }, + } as unknown as McpServer; + + registerTriggerElicitationRequestTool(mockServer); + + const handler = handlers.get("trigger-elicitation-request")!; + const result = await handler({}, { mcpReq: { send: mockSendRequest } }); + + expect(result.content[0].text).toContain("✅"); + expect(result.content[0].text).toContain("provided"); + expect(result.content[1].text).toContain("John Doe"); }); - describe('get-annotated-message', () => { - it('should return error message with high priority', async () => { - const { mockServer, handlers } = createMockServer(); - registerGetAnnotatedMessageTool(mockServer); - - const handler = handlers.get('get-annotated-message')!; - const result = await handler({ messageType: 'error', includeImage: false }); - - expect(result.content).toHaveLength(1); - expect(result.content[0].text).toBe('Error: Operation failed'); - expect(result.content[0].annotations).toEqual({ - priority: 1.0, - audience: ['user', 'assistant'] - }); - }); - - it('should return success message with medium priority', async () => { - const { mockServer, handlers } = createMockServer(); - registerGetAnnotatedMessageTool(mockServer); - - const handler = handlers.get('get-annotated-message')!; - const result = await handler({ messageType: 'success', includeImage: false }); - - expect(result.content[0].text).toBe('Operation completed successfully'); - expect(result.content[0].annotations.priority).toBe(0.7); - expect(result.content[0].annotations.audience).toEqual(['user']); - }); - - it('should return debug message with low priority', async () => { - const { mockServer, handlers } = createMockServer(); - registerGetAnnotatedMessageTool(mockServer); - - const handler = handlers.get('get-annotated-message')!; - const result = await handler({ messageType: 'debug', includeImage: false }); - - expect(result.content[0].text).toContain('Debug:'); - expect(result.content[0].annotations.priority).toBe(0.3); - expect(result.content[0].annotations.audience).toEqual(['assistant']); - }); - - it('should include annotated image when requested', async () => { - const { mockServer, handlers } = createMockServer(); - registerGetAnnotatedMessageTool(mockServer); - - const handler = handlers.get('get-annotated-message')!; - const result = await handler({ messageType: 'success', includeImage: true }); - - expect(result.content).toHaveLength(2); - expect(result.content[1].type).toBe('image'); - expect(result.content[1].annotations).toEqual({ - priority: 0.5, - audience: ['user'] - }); - }); + it("should handle decline action", async () => { + const handlers: Map = new Map(); + const mockSendRequest = vi.fn().mockResolvedValue({ + action: "decline", + }); + + const mockServer = { + registerTool: vi.fn((name: string, config: any, handler: Function) => { + handlers.set(name, handler); + }), + server: { + getClientCapabilities: vi.fn(() => ({ elicitation: {} })), + }, + } as unknown as McpServer; + + registerTriggerElicitationRequestTool(mockServer); + + const handler = handlers.get("trigger-elicitation-request")!; + const result = await handler({}, { mcpReq: { send: mockSendRequest } }); + + expect(result.content[0].text).toContain("❌"); + expect(result.content[0].text).toContain("declined"); }); - describe('trigger-long-running-operation', () => { - it('should complete operation and return result', async () => { - const { mockServer, handlers } = createMockServer(); - registerTriggerLongRunningOperationTool(mockServer); - - const handler = handlers.get('trigger-long-running-operation')!; - // Use very short duration for test - const result = await handler({ duration: 0.1, steps: 2 }, { mcpReq: { _meta: {}, id: 'test-123' } }); - - expect(result.content[0].text).toContain('Long running operation completed'); - expect(result.content[0].text).toContain('Duration: 0.1 seconds'); - expect(result.content[0].text).toContain('Steps: 2'); - }, 10000); - - it('should send progress notifications when progressToken provided', async () => { - const { mockServer, handlers } = createMockServer(); - registerTriggerLongRunningOperationTool(mockServer); - - const handler = handlers.get('trigger-long-running-operation')!; - await handler( - { duration: 0.1, steps: 2 }, - { mcpReq: { _meta: { progressToken: 'token-123' }, id: 'test-456' }, sessionId: 'session-1' } - ); - - expect(mockServer.server.notification).toHaveBeenCalledTimes(2); - expect(mockServer.server.notification).toHaveBeenCalledWith( - expect.objectContaining({ - method: 'notifications/progress', - params: expect.objectContaining({ - progressToken: 'token-123' - }) - }), - expect.any(Object) - ); - }, 10000); + it("should handle cancel action", async () => { + const handlers: Map = new Map(); + const mockSendRequest = vi.fn().mockResolvedValue({ + action: "cancel", + }); + + const mockServer = { + registerTool: vi.fn((name: string, config: any, handler: Function) => { + handlers.set(name, handler); + }), + server: { + getClientCapabilities: vi.fn(() => ({ elicitation: {} })), + }, + } as unknown as McpServer; + + registerTriggerElicitationRequestTool(mockServer); + + const handler = handlers.get("trigger-elicitation-request")!; + const result = await handler({}, { mcpReq: { send: mockSendRequest } }); + + expect(result.content[0].text).toContain("⚠️"); + expect(result.content[0].text).toContain("cancelled"); }); + }); - describe('get-resource-links', () => { - it('should return specified number of resource links', async () => { - const { mockServer, handlers } = createMockServer(); - registerGetResourceLinksTool(mockServer); - - const handler = handlers.get('get-resource-links')!; - const result = await handler({ count: 3 }); - - // 1 intro text + 3 resource links - expect(result.content).toHaveLength(4); - expect(result.content[0].type).toBe('text'); - expect(result.content[0].text).toContain('3 resource links'); - - // Check resource links - for (let i = 1; i < 4; i++) { - expect(result.content[i].type).toBe('resource_link'); - expect(result.content[i].uri).toBeDefined(); - expect(result.content[i].name).toBeDefined(); - } - }); - - it('should alternate between text and blob resources', async () => { - const { mockServer, handlers } = createMockServer(); - registerGetResourceLinksTool(mockServer); - - const handler = handlers.get('get-resource-links')!; - const result = await handler({ count: 4 }); - - // Odd IDs (1, 3) are blob, even IDs (2, 4) are text - expect(result.content[1].name).toContain('Blob'); - expect(result.content[2].name).toContain('Text'); - expect(result.content[3].name).toContain('Blob'); - expect(result.content[4].name).toContain('Text'); - }); - - it('should use default count of 3', async () => { - const { mockServer, handlers } = createMockServer(); - registerGetResourceLinksTool(mockServer); - - const handler = handlers.get('get-resource-links')!; - const result = await handler({}); - - // 1 intro text + 3 resource links (default) - expect(result.content).toHaveLength(4); - }); + describe("trigger-url-elicitation", () => { + // The error-path marker is module-level state shared across cases; reset it + // so tests are independent of order and of each other's leftover keys. + beforeEach(() => { + __resetIssuedErrorPathElicitations(); }); - describe('get-resource-reference', () => { - it('should return text resource reference', async () => { - const { mockServer, handlers } = createMockServer(); - registerGetResourceReferenceTool(mockServer); + it("should not register when client does not support URL elicitation", () => { + const handlers: Map = new Map(); + const mockServer = { + registerTool: vi.fn((name: string, config: any, handler: Function) => { + handlers.set(name, handler); + }), + server: { + getClientCapabilities: vi.fn(() => ({ elicitation: { form: {} } })), + }, + } as unknown as McpServer; - const handler = handlers.get('get-resource-reference')!; - const result = await handler({ resourceType: 'Text', resourceId: 1 }); + registerTriggerUrlElicitationTool(mockServer); - expect(result.content).toHaveLength(3); - expect(result.content[0].text).toContain('Resource 1'); - expect(result.content[1].type).toBe('resource'); - expect(result.content[1].resource.uri).toContain('text/1'); - expect(result.content[2].text).toContain('URI'); - }); + expect(mockServer.registerTool).not.toHaveBeenCalled(); + }); - it('should return blob resource reference', async () => { - const { mockServer, handlers } = createMockServer(); - registerGetResourceReferenceTool(mockServer); + it("should register when client supports URL elicitation", () => { + const handlers: Map = new Map(); + const mockServer = { + registerTool: vi.fn((name: string, config: any, handler: Function) => { + handlers.set(name, handler); + }), + server: { + getClientCapabilities: vi.fn(() => ({ elicitation: { url: {} } })), + }, + } as unknown as McpServer; - const handler = handlers.get('get-resource-reference')!; - const result = await handler({ resourceType: 'Blob', resourceId: 5 }); + registerTriggerUrlElicitationTool(mockServer); - expect(result.content[1].resource.uri).toContain('blob/5'); - }); + expect(mockServer.registerTool).toHaveBeenCalledWith( + "trigger-url-elicitation", + expect.objectContaining({ + title: "Trigger URL Elicitation Tool", + description: expect.stringContaining("URL elicitation"), + }), + expect.any(Function) + ); + }); - it('should reject invalid resource type', async () => { - const { mockServer, handlers } = createMockServer(); - registerGetResourceReferenceTool(mockServer); + it("should send URL-mode elicitation request when errorPath is false", async () => { + const handlers: Map = new Map(); + const mockSendRequest = vi.fn().mockResolvedValue({ + action: "accept", + }); - const handler = handlers.get('get-resource-reference')!; - await expect(handler({ resourceType: 'Invalid', resourceId: 1 })).rejects.toThrow('Invalid resourceType'); - }); + const mockServer = { + registerTool: vi.fn((name: string, config: any, handler: Function) => { + handlers.set(name, handler); + }), + server: { + getClientCapabilities: vi.fn(() => ({ elicitation: { url: {} } })), + }, + } as unknown as McpServer; + + registerTriggerUrlElicitationTool(mockServer); - it('should reject invalid resource ID', async () => { - const { mockServer, handlers } = createMockServer(); - registerGetResourceReferenceTool(mockServer); + const handler = handlers.get("trigger-url-elicitation")!; + const result = await handler( + { + url: "https://example.com/verify", + message: "Open this page to verify your identity", + elicitationId: "elicitation-123", + errorPath: false, + }, + { mcpReq: { send: mockSendRequest } } + ); + + expect(mockSendRequest).toHaveBeenCalledWith( + expect.objectContaining({ + method: "elicitation/create", + params: expect.objectContaining({ + mode: "url", + url: "https://example.com/verify", + message: "Open this page to verify your identity", + elicitationId: "elicitation-123", + }), + }), + expect.anything(), + expect.anything() + ); - const handler = handlers.get('get-resource-reference')!; - await expect(handler({ resourceType: 'Text', resourceId: -1 })).rejects.toThrow('Invalid resourceId'); - await expect(handler({ resourceType: 'Text', resourceId: 0 })).rejects.toThrow('Invalid resourceId'); - await expect(handler({ resourceType: 'Text', resourceId: 1.5 })).rejects.toThrow('Invalid resourceId'); - }); + expect(result.content[0].text).toContain( + "✅ User completed the URL elicitation flow." + ); }); - describe('toggle-simulated-logging', () => { - it('should start logging when not active', async () => { - const { mockServer, handlers } = createMockServer(); - registerToggleSimulatedLoggingTool(mockServer); + it("should not register when client has no elicitation capability at all", () => { + const mockServer = { + registerTool: vi.fn(), + server: { + getClientCapabilities: vi.fn(() => ({})), + }, + } as unknown as McpServer; - const handler = handlers.get('toggle-simulated-logging')!; - const result = await handler({}, { sessionId: 'test-session-1' }); + registerTriggerUrlElicitationTool(mockServer); - expect(result.content[0].text).toContain('Started'); - expect(result.content[0].text).toContain('test-session-1'); - }); + expect(mockServer.registerTool).not.toHaveBeenCalled(); + }); - it('should stop logging when already active', async () => { - const { mockServer, handlers } = createMockServer(); - registerToggleSimulatedLoggingTool(mockServer); + it("should not register when client capabilities are undefined", () => { + const mockServer = { + registerTool: vi.fn(), + server: { + getClientCapabilities: vi.fn(() => undefined), + }, + } as unknown as McpServer; - const handler = handlers.get('toggle-simulated-logging')!; + registerTriggerUrlElicitationTool(mockServer); - // First call starts logging - await handler({}, { sessionId: 'test-session-2' }); + expect(mockServer.registerTool).not.toHaveBeenCalled(); + }); - // Second call stops logging - const result = await handler({}, { sessionId: 'test-session-2' }); + it("should default the elicitationId to a random UUID when omitted", async () => { + const handlers: Map = new Map(); + const mockSendRequest = vi.fn().mockResolvedValue({ + action: "accept", + }); - expect(result.content[0].text).toContain('Stopped'); - expect(result.content[0].text).toContain('test-session-2'); - }); + const mockServer = { + registerTool: vi.fn((name: string, config: any, handler: Function) => { + handlers.set(name, handler); + }), + server: { + getClientCapabilities: vi.fn(() => ({ elicitation: { url: {} } })), + }, + } as unknown as McpServer; - it('should handle undefined sessionId', async () => { - const { mockServer, handlers } = createMockServer(); - registerToggleSimulatedLoggingTool(mockServer); + registerTriggerUrlElicitationTool(mockServer); - const handler = handlers.get('toggle-simulated-logging')!; - const result = await handler({}, {}); + const handler = handlers.get("trigger-url-elicitation")!; + await handler( + { + url: "https://example.com/verify", + message: "Open this page to verify your identity", + errorPath: false, + }, + { mcpReq: { send: mockSendRequest } } + ); - expect(result.content[0].text).toContain('Started'); - }); + const sentParams = mockSendRequest.mock.calls[0][0].params; + expect(sentParams.elicitationId).toMatch( + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/ + ); }); - describe('toggle-subscriber-updates', () => { - it('should start updates when not active', async () => { - const { mockServer, handlers } = createMockServer(); - registerToggleSubscriberUpdatesTool(mockServer); + it("should report a declined URL elicitation", async () => { + const handlers: Map = new Map(); + const mockSendRequest = vi.fn().mockResolvedValue({ action: "decline" }); + + const mockServer = { + registerTool: vi.fn((name: string, config: any, handler: Function) => { + handlers.set(name, handler); + }), + server: { + getClientCapabilities: vi.fn(() => ({ elicitation: { url: {} } })), + }, + } as unknown as McpServer; + + registerTriggerUrlElicitationTool(mockServer); + + const handler = handlers.get("trigger-url-elicitation")!; + const result = await handler( + { + url: "https://example.com/verify", + message: "Open this page to verify your identity", + elicitationId: "elicitation-123", + errorPath: false, + }, + { mcpReq: { send: mockSendRequest } } + ); - const handler = handlers.get('toggle-subscriber-updates')!; - const result = await handler({}, { sessionId: 'sub-session-1' }); + expect(result.content[0].text).toContain( + "❌ User declined to open the URL" + ); + }); - expect(result.content[0].text).toContain('Started'); - expect(result.content[0].text).toContain('sub-session-1'); - }); + it("should report a cancelled URL elicitation", async () => { + const handlers: Map = new Map(); + const mockSendRequest = vi.fn().mockResolvedValue({ action: "cancel" }); - it('should stop updates when already active', async () => { - const { mockServer, handlers } = createMockServer(); - registerToggleSubscriberUpdatesTool(mockServer); + const mockServer = { + registerTool: vi.fn((name: string, config: any, handler: Function) => { + handlers.set(name, handler); + }), + server: { + getClientCapabilities: vi.fn(() => ({ elicitation: { url: {} } })), + }, + } as unknown as McpServer; - const handler = handlers.get('toggle-subscriber-updates')!; + registerTriggerUrlElicitationTool(mockServer); - // First call starts updates - await handler({}, { sessionId: 'sub-session-2' }); + const handler = handlers.get("trigger-url-elicitation")!; + const result = await handler( + { + url: "https://example.com/verify", + message: "Open this page to verify your identity", + elicitationId: "elicitation-123", + errorPath: false, + }, + { mcpReq: { send: mockSendRequest } } + ); - // Second call stops updates - const result = await handler({}, { sessionId: 'sub-session-2' }); + expect(result.content[0].text).toContain( + "⚠️ User cancelled the URL elicitation" + ); + }); - expect(result.content[0].text).toContain('Stopped'); - expect(result.content[0].text).toContain('sub-session-2'); - }); + it("should throw MCP error -32042 with a prerequisite elicitation pointing at a different URL when errorPath is true", async () => { + const handlers: Map = new Map(); + const mockServer = { + registerTool: vi.fn((name: string, config: any, handler: Function) => { + handlers.set(name, handler); + }), + server: { + getClientCapabilities: vi.fn(() => ({ elicitation: { url: {} } })), + }, + } as unknown as McpServer; + + registerTriggerUrlElicitationTool(mockServer); + + const handler = handlers.get("trigger-url-elicitation")!; + + expect.assertions(5); + + try { + await handler( + { + url: "https://example.com/connect", + message: "Authorization is required to continue.", + elicitationId: "elicitation-xyz", + errorPath: true, + }, + {} + ); + } catch (error: any) { + expect(error.code).toBe(-32042); + const prerequisite = error.data.elicitations[0]; + expect(prerequisite.mode).toBe("url"); + // The prerequisite must NOT reuse the failing URL, otherwise the client + // would complete it, retry, hit the same error, and loop forever. + expect(prerequisite.url).toBe("https://modelcontextprotocol.io"); + expect(prerequisite.url).not.toBe("https://example.com/connect"); + // It carries its own elicitation id for the prerequisite itself. + expect(typeof prerequisite.elicitationId).toBe("string"); + } }); - describe('trigger-sampling-request', () => { - it('should not register when client does not support sampling', () => { - const { mockServer } = createMockServer(); - registerTriggerSamplingRequestTool(mockServer); - - // Tool should not be registered since mock server returns empty capabilities - expect(mockServer.registerTool).not.toHaveBeenCalled(); - }); - - it('should register when client supports sampling', () => { - const handlers: Map = new Map(); - const mockServer = { - registerTool: vi.fn((name: string, config: any, handler: Function) => { - handlers.set(name, handler); - }), - server: { - getClientCapabilities: vi.fn(() => ({ sampling: {} })) - } - } as unknown as McpServer; - - registerTriggerSamplingRequestTool(mockServer); - - expect(mockServer.registerTool).toHaveBeenCalledWith( - 'trigger-sampling-request', - expect.objectContaining({ - title: 'Trigger Sampling Request Tool', - description: expect.stringContaining('Sampling') - }), - expect.any(Function) - ); - }); - - it('should send sampling request and return result', async () => { - const handlers: Map = new Map(); - const mockSendRequest = vi.fn().mockResolvedValue({ - model: 'test-model', - content: { type: 'text', text: 'LLM response' } - }); - - const mockServer = { - registerTool: vi.fn((name: string, config: any, handler: Function) => { - handlers.set(name, handler); - }), - server: { - getClientCapabilities: vi.fn(() => ({ sampling: {} })) - } - } as unknown as McpServer; - - registerTriggerSamplingRequestTool(mockServer); - - const handler = handlers.get('trigger-sampling-request')!; - const result = await handler({ prompt: 'Test prompt', maxTokens: 50 }, { mcpReq: { send: mockSendRequest } }); - - expect(mockSendRequest).toHaveBeenCalledWith( - expect.objectContaining({ - method: 'sampling/createMessage', - params: expect.objectContaining({ - maxTokens: 50 - }) - }), - expect.anything() - ); - expect(result.content[0].text).toContain('LLM sampling result'); - }); + it("should ignore errorPath and take the request path when the same call is retried after the prerequisite", async () => { + const handlers: Map = new Map(); + const mockSendRequest = vi.fn().mockResolvedValue({ action: "accept" }); + + const mockServer = { + registerTool: vi.fn((name: string, config: any, handler: Function) => { + handlers.set(name, handler); + }), + server: { + getClientCapabilities: vi.fn(() => ({ elicitation: { url: {} } })), + }, + } as unknown as McpServer; + + registerTriggerUrlElicitationTool(mockServer); + + const handler = handlers.get("trigger-url-elicitation")!; + // A real client retries with the *same* arguments and does not echo the + // prerequisite's elicitationId. Note these args omit elicitationId, so the + // correlation must rely on stable inputs (session + url), not a per-call + // random id. + const args = { + url: "https://example.com/connect", + message: "Authorization is required to continue.", + errorPath: true, + }; + const extra = { + sessionId: "session-1", + mcpReq: { send: mockSendRequest }, + }; + + // First call: error path issues the prerequisite and throws -32042. + let prerequisiteUrl: string | undefined; + try { + await handler(args, extra); + throw new Error("expected first call to throw"); + } catch (error: any) { + expect(error.code).toBe(-32042); + prerequisiteUrl = error.data.elicitations[0].url; + expect(prerequisiteUrl).toBe("https://modelcontextprotocol.io"); + expect(mockSendRequest).not.toHaveBeenCalled(); + } + + // Plain retry with identical arguments: errorPath is ignored and the call + // proceeds via the request path instead of throwing the prerequisite again. + const result = await handler({ ...args }, extra); + + expect(mockSendRequest).toHaveBeenCalledWith( + expect.objectContaining({ + method: "elicitation/create", + params: expect.objectContaining({ + mode: "url", + url: "https://example.com/connect", + }), + }), + expect.anything(), + expect.anything() + ); + expect(result.content[0].text).toContain( + "✅ User completed the URL elicitation flow." + ); }); + }); + + describe("get-roots-list", () => { + it("should not register when client does not support roots", () => { + const { mockServer } = createMockServer(); + registerGetRootsListTool(mockServer); - describe('trigger-elicitation-request', () => { - it('should not register when client does not support elicitation', () => { - const { mockServer } = createMockServer(); - registerTriggerElicitationRequestTool(mockServer); - - expect(mockServer.registerTool).not.toHaveBeenCalled(); - }); - - it('should register when client supports elicitation', () => { - const handlers: Map = new Map(); - const mockServer = { - registerTool: vi.fn((name: string, config: any, handler: Function) => { - handlers.set(name, handler); - }), - server: { - getClientCapabilities: vi.fn(() => ({ elicitation: {} })) - } - } as unknown as McpServer; - - registerTriggerElicitationRequestTool(mockServer); - - expect(mockServer.registerTool).toHaveBeenCalledWith( - 'trigger-elicitation-request', - expect.objectContaining({ - title: 'Trigger Elicitation Request Tool', - description: expect.stringContaining('Elicitation') - }), - expect.any(Function) - ); - }); - - it('should handle accept action with user content', async () => { - const handlers: Map = new Map(); - const mockSendRequest = vi.fn().mockResolvedValue({ - action: 'accept', - content: { - name: 'John Doe', - check: true, - email: 'john@example.com' - } - }); - - const mockServer = { - registerTool: vi.fn((name: string, config: any, handler: Function) => { - handlers.set(name, handler); - }), - server: { - getClientCapabilities: vi.fn(() => ({ elicitation: {} })) - } - } as unknown as McpServer; - - registerTriggerElicitationRequestTool(mockServer); - - const handler = handlers.get('trigger-elicitation-request')!; - const result = await handler({}, { mcpReq: { send: mockSendRequest } }); - - expect(result.content[0].text).toContain('✅'); - expect(result.content[0].text).toContain('provided'); - expect(result.content[1].text).toContain('John Doe'); - }); - - it('should handle decline action', async () => { - const handlers: Map = new Map(); - const mockSendRequest = vi.fn().mockResolvedValue({ - action: 'decline' - }); - - const mockServer = { - registerTool: vi.fn((name: string, config: any, handler: Function) => { - handlers.set(name, handler); - }), - server: { - getClientCapabilities: vi.fn(() => ({ elicitation: {} })) - } - } as unknown as McpServer; - - registerTriggerElicitationRequestTool(mockServer); - - const handler = handlers.get('trigger-elicitation-request')!; - const result = await handler({}, { mcpReq: { send: mockSendRequest } }); - - expect(result.content[0].text).toContain('❌'); - expect(result.content[0].text).toContain('declined'); - }); - - it('should handle cancel action', async () => { - const handlers: Map = new Map(); - const mockSendRequest = vi.fn().mockResolvedValue({ - action: 'cancel' - }); - - const mockServer = { - registerTool: vi.fn((name: string, config: any, handler: Function) => { - handlers.set(name, handler); - }), - server: { - getClientCapabilities: vi.fn(() => ({ elicitation: {} })) - } - } as unknown as McpServer; - - registerTriggerElicitationRequestTool(mockServer); - - const handler = handlers.get('trigger-elicitation-request')!; - const result = await handler({}, { mcpReq: { send: mockSendRequest } }); - - expect(result.content[0].text).toContain('⚠️'); - expect(result.content[0].text).toContain('cancelled'); - }); + expect(mockServer.registerTool).not.toHaveBeenCalled(); }); - describe('trigger-url-elicitation', () => { - // The error-path marker is module-level state shared across cases; reset it - // so tests are independent of order and of each other's leftover keys. - beforeEach(() => { - __resetIssuedErrorPathElicitations(); - }); - - it('should not register when client does not support URL elicitation', () => { - const handlers: Map = new Map(); - const mockServer = { - registerTool: vi.fn((name: string, config: any, handler: Function) => { - handlers.set(name, handler); - }), - server: { - getClientCapabilities: vi.fn(() => ({ elicitation: { form: {} } })) - } - } as unknown as McpServer; - - registerTriggerUrlElicitationTool(mockServer); - - expect(mockServer.registerTool).not.toHaveBeenCalled(); - }); - - it('should register when client supports URL elicitation', () => { - const handlers: Map = new Map(); - const mockServer = { - registerTool: vi.fn((name: string, config: any, handler: Function) => { - handlers.set(name, handler); - }), - server: { - getClientCapabilities: vi.fn(() => ({ elicitation: { url: {} } })) - } - } as unknown as McpServer; - - registerTriggerUrlElicitationTool(mockServer); - - expect(mockServer.registerTool).toHaveBeenCalledWith( - 'trigger-url-elicitation', - expect.objectContaining({ - title: 'Trigger URL Elicitation Tool', - description: expect.stringContaining('URL elicitation') - }), - expect.any(Function) - ); - }); - - it('should send URL-mode elicitation request when errorPath is false', async () => { - const handlers: Map = new Map(); - const mockSendRequest = vi.fn().mockResolvedValue({ - action: 'accept' - }); - - const mockServer = { - registerTool: vi.fn((name: string, config: any, handler: Function) => { - handlers.set(name, handler); - }), - server: { - getClientCapabilities: vi.fn(() => ({ elicitation: { url: {} } })) - } - } as unknown as McpServer; - - registerTriggerUrlElicitationTool(mockServer); - - const handler = handlers.get('trigger-url-elicitation')!; - const result = await handler( - { - url: 'https://example.com/verify', - message: 'Open this page to verify your identity', - elicitationId: 'elicitation-123', - errorPath: false - }, - { mcpReq: { send: mockSendRequest } } - ); - - expect(mockSendRequest).toHaveBeenCalledWith( - expect.objectContaining({ - method: 'elicitation/create', - params: expect.objectContaining({ - mode: 'url', - url: 'https://example.com/verify', - message: 'Open this page to verify your identity', - elicitationId: 'elicitation-123' - }) - }), - expect.anything(), - expect.anything() - ); - - expect(result.content[0].text).toContain('✅ User completed the URL elicitation flow.'); - }); - - it('should not register when client has no elicitation capability at all', () => { - const mockServer = { - registerTool: vi.fn(), - server: { - getClientCapabilities: vi.fn(() => ({})) - } - } as unknown as McpServer; - - registerTriggerUrlElicitationTool(mockServer); - - expect(mockServer.registerTool).not.toHaveBeenCalled(); - }); - - it('should not register when client capabilities are undefined', () => { - const mockServer = { - registerTool: vi.fn(), - server: { - getClientCapabilities: vi.fn(() => undefined) - } - } as unknown as McpServer; - - registerTriggerUrlElicitationTool(mockServer); - - expect(mockServer.registerTool).not.toHaveBeenCalled(); - }); - - it('should default the elicitationId to a random UUID when omitted', async () => { - const handlers: Map = new Map(); - const mockSendRequest = vi.fn().mockResolvedValue({ - action: 'accept' - }); - - const mockServer = { - registerTool: vi.fn((name: string, config: any, handler: Function) => { - handlers.set(name, handler); - }), - server: { - getClientCapabilities: vi.fn(() => ({ elicitation: { url: {} } })) - } - } as unknown as McpServer; - - registerTriggerUrlElicitationTool(mockServer); - - const handler = handlers.get('trigger-url-elicitation')!; - await handler( - { - url: 'https://example.com/verify', - message: 'Open this page to verify your identity', - errorPath: false - }, - { mcpReq: { send: mockSendRequest } } - ); - - const sentParams = mockSendRequest.mock.calls[0][0].params; - expect(sentParams.elicitationId).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/); - }); - - it('should report a declined URL elicitation', async () => { - const handlers: Map = new Map(); - const mockSendRequest = vi.fn().mockResolvedValue({ action: 'decline' }); - - const mockServer = { - registerTool: vi.fn((name: string, config: any, handler: Function) => { - handlers.set(name, handler); - }), - server: { - getClientCapabilities: vi.fn(() => ({ elicitation: { url: {} } })) - } - } as unknown as McpServer; - - registerTriggerUrlElicitationTool(mockServer); - - const handler = handlers.get('trigger-url-elicitation')!; - const result = await handler( - { - url: 'https://example.com/verify', - message: 'Open this page to verify your identity', - elicitationId: 'elicitation-123', - errorPath: false - }, - { mcpReq: { send: mockSendRequest } } - ); - - expect(result.content[0].text).toContain('❌ User declined to open the URL'); - }); - - it('should report a cancelled URL elicitation', async () => { - const handlers: Map = new Map(); - const mockSendRequest = vi.fn().mockResolvedValue({ action: 'cancel' }); - - const mockServer = { - registerTool: vi.fn((name: string, config: any, handler: Function) => { - handlers.set(name, handler); - }), - server: { - getClientCapabilities: vi.fn(() => ({ elicitation: { url: {} } })) - } - } as unknown as McpServer; - - registerTriggerUrlElicitationTool(mockServer); - - const handler = handlers.get('trigger-url-elicitation')!; - const result = await handler( - { - url: 'https://example.com/verify', - message: 'Open this page to verify your identity', - elicitationId: 'elicitation-123', - errorPath: false - }, - { mcpReq: { send: mockSendRequest } } - ); - - expect(result.content[0].text).toContain('⚠️ User cancelled the URL elicitation'); - }); - - it('should throw MCP error -32042 with a prerequisite elicitation pointing at a different URL when errorPath is true', async () => { - const handlers: Map = new Map(); - const mockServer = { - registerTool: vi.fn((name: string, config: any, handler: Function) => { - handlers.set(name, handler); - }), - server: { - getClientCapabilities: vi.fn(() => ({ elicitation: { url: {} } })) - } - } as unknown as McpServer; - - registerTriggerUrlElicitationTool(mockServer); - - const handler = handlers.get('trigger-url-elicitation')!; - - expect.assertions(5); - - try { - await handler( - { - url: 'https://example.com/connect', - message: 'Authorization is required to continue.', - elicitationId: 'elicitation-xyz', - errorPath: true - }, - {} - ); - } catch (error: any) { - expect(error.code).toBe(-32042); - const prerequisite = error.data.elicitations[0]; - expect(prerequisite.mode).toBe('url'); - // The prerequisite must NOT reuse the failing URL, otherwise the client - // would complete it, retry, hit the same error, and loop forever. - expect(prerequisite.url).toBe('https://modelcontextprotocol.io'); - expect(prerequisite.url).not.toBe('https://example.com/connect'); - // It carries its own elicitation id for the prerequisite itself. - expect(typeof prerequisite.elicitationId).toBe('string'); - } - }); - - it('should ignore errorPath and take the request path when the same call is retried after the prerequisite', async () => { - const handlers: Map = new Map(); - const mockSendRequest = vi.fn().mockResolvedValue({ action: 'accept' }); - - const mockServer = { - registerTool: vi.fn((name: string, config: any, handler: Function) => { - handlers.set(name, handler); - }), - server: { - getClientCapabilities: vi.fn(() => ({ elicitation: { url: {} } })) - } - } as unknown as McpServer; - - registerTriggerUrlElicitationTool(mockServer); - - const handler = handlers.get('trigger-url-elicitation')!; - // A real client retries with the *same* arguments and does not echo the - // prerequisite's elicitationId. Note these args omit elicitationId, so the - // correlation must rely on stable inputs (session + url), not a per-call - // random id. - const args = { - url: 'https://example.com/connect', - message: 'Authorization is required to continue.', - errorPath: true - }; - const extra = { sessionId: 'session-1', mcpReq: { send: mockSendRequest } }; - - // First call: error path issues the prerequisite and throws -32042. - let prerequisiteUrl: string | undefined; - try { - await handler(args, extra); - throw new Error('expected first call to throw'); - } catch (error: any) { - expect(error.code).toBe(-32042); - prerequisiteUrl = error.data.elicitations[0].url; - expect(prerequisiteUrl).toBe('https://modelcontextprotocol.io'); - expect(mockSendRequest).not.toHaveBeenCalled(); - } - - // Plain retry with identical arguments: errorPath is ignored and the call - // proceeds via the request path instead of throwing the prerequisite again. - const result = await handler({ ...args }, extra); - - expect(mockSendRequest).toHaveBeenCalledWith( - expect.objectContaining({ - method: 'elicitation/create', - params: expect.objectContaining({ - mode: 'url', - url: 'https://example.com/connect' - }) - }), - expect.anything(), - expect.anything() - ); - expect(result.content[0].text).toContain('✅ User completed the URL elicitation flow.'); - }); + it("should register when client supports roots", () => { + const handlers: Map = new Map(); + const mockServer = { + registerTool: vi.fn((name: string, config: any, handler: Function) => { + handlers.set(name, handler); + }), + server: { + getClientCapabilities: vi.fn(() => ({ roots: {} })), + }, + } as unknown as McpServer; + + registerGetRootsListTool(mockServer); + + expect(mockServer.registerTool).toHaveBeenCalledWith( + "get-roots-list", + expect.objectContaining({ + title: "Get Roots List Tool", + description: expect.stringContaining("roots"), + }), + expect.any(Function) + ); + }); + }); + + describe("gzip-file-as-resource", () => { + it("should compress data URI and return resource link", async () => { + const registeredResources: any[] = []; + const mockServer = { + registerTool: vi.fn(), + registerResource: vi.fn((...args) => { + registeredResources.push(args); + }), + } as unknown as McpServer; + + // Get the handler + let handler: Function | null = null; + (mockServer.registerTool as any).mockImplementation( + (name: string, config: any, h: Function) => { + handler = h; + } + ); + + registerGZipFileAsResourceTool(mockServer); + + // Create a data URI with test content + const testContent = "Hello, World!"; + const dataUri = `data:text/plain;base64,${Buffer.from( + testContent + ).toString("base64")}`; + + const result = await handler!({ + name: "test.txt.gz", + data: dataUri, + outputType: "resourceLink", + }); + + expect(result.content[0].type).toBe("resource_link"); + expect(result.content[0].uri).toContain("test.txt.gz"); }); - describe('get-roots-list', () => { - it('should not register when client does not support roots', () => { - const { mockServer } = createMockServer(); - registerGetRootsListTool(mockServer); - - expect(mockServer.registerTool).not.toHaveBeenCalled(); - }); - - it('should register when client supports roots', () => { - const handlers: Map = new Map(); - const mockServer = { - registerTool: vi.fn((name: string, config: any, handler: Function) => { - handlers.set(name, handler); - }), - server: { - getClientCapabilities: vi.fn(() => ({ roots: {} })) - } - } as unknown as McpServer; - - registerGetRootsListTool(mockServer); - - expect(mockServer.registerTool).toHaveBeenCalledWith( - 'get-roots-list', - expect.objectContaining({ - title: 'Get Roots List Tool', - description: expect.stringContaining('roots') - }), - expect.any(Function) - ); - }); + it("should return resource directly when outputType is resource", async () => { + const mockServer = { + registerTool: vi.fn(), + registerResource: vi.fn(), + } as unknown as McpServer; + + let handler: Function | null = null; + (mockServer.registerTool as any).mockImplementation( + (name: string, config: any, h: Function) => { + handler = h; + } + ); + + registerGZipFileAsResourceTool(mockServer); + + const testContent = "Test content for compression"; + const dataUri = `data:text/plain;base64,${Buffer.from( + testContent + ).toString("base64")}`; + + const result = await handler!({ + name: "output.gz", + data: dataUri, + outputType: "resource", + }); + + expect(result.content[0].type).toBe("resource"); + expect(result.content[0].resource.mimeType).toBe("application/gzip"); + expect(result.content[0].resource.blob).toBeDefined(); }); - describe('gzip-file-as-resource', () => { - it('should compress data URI and return resource link', async () => { - const registeredResources: any[] = []; - const mockServer = { - registerTool: vi.fn(), - registerResource: vi.fn((...args) => { - registeredResources.push(args); - }) - } as unknown as McpServer; - - // Get the handler - let handler: Function | null = null; - (mockServer.registerTool as any).mockImplementation((name: string, config: any, h: Function) => { - handler = h; - }); - - registerGZipFileAsResourceTool(mockServer); - - // Create a data URI with test content - const testContent = 'Hello, World!'; - const dataUri = `data:text/plain;base64,${Buffer.from(testContent).toString('base64')}`; - - const result = await handler!({ name: 'test.txt.gz', data: dataUri, outputType: 'resourceLink' }); - - expect(result.content[0].type).toBe('resource_link'); - expect(result.content[0].uri).toContain('test.txt.gz'); - }); - - it('should return resource directly when outputType is resource', async () => { - const mockServer = { - registerTool: vi.fn(), - registerResource: vi.fn() - } as unknown as McpServer; - - let handler: Function | null = null; - (mockServer.registerTool as any).mockImplementation((name: string, config: any, h: Function) => { - handler = h; - }); - - registerGZipFileAsResourceTool(mockServer); - - const testContent = 'Test content for compression'; - const dataUri = `data:text/plain;base64,${Buffer.from(testContent).toString('base64')}`; - - const result = await handler!({ name: 'output.gz', data: dataUri, outputType: 'resource' }); - - expect(result.content[0].type).toBe('resource'); - expect(result.content[0].resource.mimeType).toBe('application/gzip'); - expect(result.content[0].resource.blob).toBeDefined(); - }); - - it('should reject unsupported URL protocols', async () => { - const mockServer = { - registerTool: vi.fn(), - registerResource: vi.fn() - } as unknown as McpServer; - - let handler: Function | null = null; - (mockServer.registerTool as any).mockImplementation((name: string, config: any, h: Function) => { - handler = h; - }); - - registerGZipFileAsResourceTool(mockServer); - - await expect(handler!({ name: 'test.gz', data: 'ftp://example.com/file.txt', outputType: 'resource' })).rejects.toThrow( - 'Unsupported URL protocol' - ); - }); + it("should reject unsupported URL protocols", async () => { + const mockServer = { + registerTool: vi.fn(), + registerResource: vi.fn(), + } as unknown as McpServer; + + let handler: Function | null = null; + (mockServer.registerTool as any).mockImplementation( + (name: string, config: any, h: Function) => { + handler = h; + } + ); + + registerGZipFileAsResourceTool(mockServer); + + await expect( + handler!({ + name: "test.gz", + data: "ftp://example.com/file.txt", + outputType: "resource", + }) + ).rejects.toThrow("Unsupported URL protocol"); }); + }); }); diff --git a/src/everything/docs/architecture.md b/src/everything/docs/architecture.md index 07f4ec2864..728cfd4010 100644 --- a/src/everything/docs/architecture.md +++ b/src/everything/docs/architecture.md @@ -29,9 +29,9 @@ resource subscriptions and simulated logging. ## Build and Distribution -- TypeScript sources are compiled into `dist/` via `npm run build`. -- The `build` script copies `docs/` into `dist/` so instruction files ship alongside the compiled server. -- The CLI bin is configured in `package.json` as `mcp-server-everything` → `dist/index.js`. +- TypeScript sources are compiled into `dist/` via `npm run build`. +- The `build` script copies `docs/` into `dist/` so instruction files ship alongside the compiled server. +- The CLI bin is configured in `package.json` as `mcp-server-everything` → `dist/index.js`. ## [Project Structure](structure.md) diff --git a/src/everything/docs/extension.md b/src/everything/docs/extension.md index 206026b0f8..1d77730448 100644 --- a/src/everything/docs/extension.md +++ b/src/everything/docs/extension.md @@ -9,15 +9,15 @@ ## Adding Tools -- Create a new file under `tools/` with your `registerXTool(server)` function that registers the tool via `server.registerTool(...)`. -- Export and call it from `tools/index.ts` inside `registerTools(server)`. +- Create a new file under `tools/` with your `registerXTool(server)` function that registers the tool via `server.registerTool(...)`. +- Export and call it from `tools/index.ts` inside `registerTools(server)`. ## Adding Prompts -- Create a new file under `prompts/` with your `registerXPrompt(server)` function that registers the prompt via `server.registerPrompt(...)`. -- Export and call it from `prompts/index.ts` inside `registerPrompts(server)`. +- Create a new file under `prompts/` with your `registerXPrompt(server)` function that registers the prompt via `server.registerPrompt(...)`. +- Export and call it from `prompts/index.ts` inside `registerPrompts(server)`. ## Adding Resources -- Create a new file under `resources/` with your `registerXResources(server)` function using `server.registerResource(...)` (optionally with `ResourceTemplate`). -- Export and call it from `resources/index.ts` inside `registerResources(server)`. +- Create a new file under `resources/` with your `registerXResources(server)` function using `server.registerResource(...)` (optionally with `ResourceTemplate`). +- Export and call it from `resources/index.ts` inside `registerResources(server)`. diff --git a/src/everything/docs/features.md b/src/everything/docs/features.md index b9b5d057c6..a5429ac600 100644 --- a/src/everything/docs/features.md +++ b/src/everything/docs/features.md @@ -9,60 +9,60 @@ ## Tools -- `echo` (tools/echo.ts): Echoes the provided `message: string`. Uses Zod to validate inputs. -- `get-annotated-message` (tools/get-annotated-message.ts): Returns a `text` message annotated with `priority` and `audience` based on `messageType` (`error`, `success`, or `debug`); can optionally include an annotated `image`. -- `get-env` (tools/get-env.ts): Returns all environment variables from the running process as pretty-printed JSON text. -- `get-resource-links` (tools/get-resource-links.ts): Returns an intro `text` block followed by multiple `resource_link` items. For a requested `count` (1–10), alternates between dynamic Text and Blob resources using URIs from `resources/templates.ts`. -- `get-resource-reference` (tools/get-resource-reference.ts): Accepts `resourceType` (`text` or `blob`) and `resourceId` (positive integer). Returns a concrete `resource` content block (with its `uri`, `mimeType`, and data) with surrounding explanatory `text`. -- `get-roots-list` (tools/get-roots-list.ts): Returns the last list of roots sent by the client. -- `gzip-file-as-resource` (tools/gzip-file-as-resource.ts): Accepts a `name` and `data` (URL or data URI), fetches the data subject to size/time/domain constraints, compresses it, registers it as a session resource at `demo://resource/session/` with `mimeType: application/gzip`, and returns either a `resource_link` (default) or an inline `resource` depending on `outputType`. -- `get-structured-content` (tools/get-structured-content.ts): Demonstrates structured responses. Accepts `location` input and returns both backward‑compatible `content` (a `text` block containing JSON) and `structuredContent` validated by an `outputSchema` (temperature, conditions, humidity). -- `get-sum` (tools/get-sum.ts): For two numbers `a` and `b` calculates and returns their sum. Uses Zod to validate inputs. -- `get-tiny-image` (tools/get-tiny-image.ts): Returns a tiny PNG MCP logo as an `image` content item with brief descriptive text before and after. -- `trigger-long-running-operation` (tools/trigger-long-running-operation.ts): Simulates a multi-step operation over a given `duration` and number of `steps`; reports progress via `notifications/progress` when a `progressToken` is provided by the client. -- `toggle-simulated-logging` (tools/toggle-simulated-logging.ts): Starts or stops simulated, random‑leveled logging for the invoking session. Respects the client’s selected minimum logging level. -- `toggle-subscriber-updates` (tools/toggle-subscriber-updates.ts): Starts or stops simulated resource update notifications for URIs the invoking session has subscribed to. -- `trigger-elicitation-request` (tools/trigger-elicitation-request.ts): Issues an `elicitation/create` request using form-mode fields (strings, numbers, booleans, enums, and format validation) and returns the resulting action/content. -- `trigger-url-elicitation` (tools/trigger-url-elicitation.ts): Issues an `elicitation/create` request in URL mode (`mode: "url"`) with an `elicitationId`, or throws MCP error `-32042` (`UrlElicitationRequiredError`) when `errorPath=true`. On the error path the prerequisite elicitation it returns points at a different URL than the failing request (`https://modelcontextprotocol.io`); when the client satisfies it and retries the same call, the retry ignores `errorPath` and proceeds via the request path, so the client does not loop on the same error. The retry marker is one-shot per `(session, url, elicitationId)`: it is cleared on the recognized retry, so re-running the error path with identical arguments without an intervening prerequisite is treated as a retry and proceeds. Requires client capability `elicitation.url`. -- `trigger-sampling-request` (tools/trigger-sampling-request.ts): Issues a `sampling/createMessage` request to the client/LLM using provided `prompt` and optional generation controls; returns the LLM's response payload. -- `simulate-research-query` (tools/simulate-research-query.ts): Demonstrates MCP Tasks (SEP-1686) with a simulated multi-stage research operation. Accepts `topic` and `ambiguous` parameters. Returns a task that progresses through stages with status updates. If `ambiguous` is true and client supports elicitation, sends an elicitation request directly to gather clarification before completing. -- `trigger-sampling-request-async` (tools/trigger-sampling-request-async.ts): Demonstrates bidirectional tasks where the server sends a sampling request that the client executes as a background task. Server polls for status and retrieves the LLM result when complete. Requires client to support `tasks.requests.sampling.createMessage`. -- `trigger-elicitation-request-async` (tools/trigger-elicitation-request-async.ts): Demonstrates bidirectional tasks where the server sends an elicitation request that the client executes as a background task. Server polls while waiting for user input. Requires client to support `tasks.requests.elicitation.create`. +- `echo` (tools/echo.ts): Echoes the provided `message: string`. Uses Zod to validate inputs. +- `get-annotated-message` (tools/get-annotated-message.ts): Returns a `text` message annotated with `priority` and `audience` based on `messageType` (`error`, `success`, or `debug`); can optionally include an annotated `image`. +- `get-env` (tools/get-env.ts): Returns all environment variables from the running process as pretty-printed JSON text. +- `get-resource-links` (tools/get-resource-links.ts): Returns an intro `text` block followed by multiple `resource_link` items. For a requested `count` (1–10), alternates between dynamic Text and Blob resources using URIs from `resources/templates.ts`. +- `get-resource-reference` (tools/get-resource-reference.ts): Accepts `resourceType` (`text` or `blob`) and `resourceId` (positive integer). Returns a concrete `resource` content block (with its `uri`, `mimeType`, and data) with surrounding explanatory `text`. +- `get-roots-list` (tools/get-roots-list.ts): Returns the last list of roots sent by the client. +- `gzip-file-as-resource` (tools/gzip-file-as-resource.ts): Accepts a `name` and `data` (URL or data URI), fetches the data subject to size/time/domain constraints, compresses it, registers it as a session resource at `demo://resource/session/` with `mimeType: application/gzip`, and returns either a `resource_link` (default) or an inline `resource` depending on `outputType`. +- `get-structured-content` (tools/get-structured-content.ts): Demonstrates structured responses. Accepts `location` input and returns both backward‑compatible `content` (a `text` block containing JSON) and `structuredContent` validated by an `outputSchema` (temperature, conditions, humidity). +- `get-sum` (tools/get-sum.ts): For two numbers `a` and `b` calculates and returns their sum. Uses Zod to validate inputs. +- `get-tiny-image` (tools/get-tiny-image.ts): Returns a tiny PNG MCP logo as an `image` content item with brief descriptive text before and after. +- `trigger-long-running-operation` (tools/trigger-long-running-operation.ts): Simulates a multi-step operation over a given `duration` and number of `steps`; reports progress via `notifications/progress` when a `progressToken` is provided by the client. +- `toggle-simulated-logging` (tools/toggle-simulated-logging.ts): Starts or stops simulated, random‑leveled logging for the invoking session. Respects the client’s selected minimum logging level. +- `toggle-subscriber-updates` (tools/toggle-subscriber-updates.ts): Starts or stops simulated resource update notifications for URIs the invoking session has subscribed to. +- `trigger-elicitation-request` (tools/trigger-elicitation-request.ts): Issues an `elicitation/create` request using form-mode fields (strings, numbers, booleans, enums, and format validation) and returns the resulting action/content. +- `trigger-url-elicitation` (tools/trigger-url-elicitation.ts): Issues an `elicitation/create` request in URL mode (`mode: "url"`) with an `elicitationId`, or throws MCP error `-32042` (`UrlElicitationRequiredError`) when `errorPath=true`. On the error path the prerequisite elicitation it returns points at a different URL than the failing request (`https://modelcontextprotocol.io`); when the client satisfies it and retries the same call, the retry ignores `errorPath` and proceeds via the request path, so the client does not loop on the same error. The retry marker is one-shot per `(session, url, elicitationId)`: it is cleared on the recognized retry, so re-running the error path with identical arguments without an intervening prerequisite is treated as a retry and proceeds. Requires client capability `elicitation.url`. +- `trigger-sampling-request` (tools/trigger-sampling-request.ts): Issues a `sampling/createMessage` request to the client/LLM using provided `prompt` and optional generation controls; returns the LLM's response payload. +- `simulate-research-query` (tools/simulate-research-query.ts): Demonstrates MCP Tasks (SEP-1686) with a simulated multi-stage research operation. Accepts `topic` and `ambiguous` parameters. Returns a task that progresses through stages with status updates. If `ambiguous` is true and client supports elicitation, sends an elicitation request directly to gather clarification before completing. +- `trigger-sampling-request-async` (tools/trigger-sampling-request-async.ts): Demonstrates bidirectional tasks where the server sends a sampling request that the client executes as a background task. Server polls for status and retrieves the LLM result when complete. Requires client to support `tasks.requests.sampling.createMessage`. +- `trigger-elicitation-request-async` (tools/trigger-elicitation-request-async.ts): Demonstrates bidirectional tasks where the server sends an elicitation request that the client executes as a background task. Server polls while waiting for user input. Requires client to support `tasks.requests.elicitation.create`. ## Prompts -- `simple-prompt` (prompts/simple.ts): No-argument prompt that returns a static user message. -- `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 `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`. +- `simple-prompt` (prompts/simple.ts): No-argument prompt that returns a static user message. +- `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 `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 Documents: `demo://resource/static/document/` (serves files from `src/everything/docs/` as static file-based resources) -- Session Scoped: `demo://resource/session/` (per-session resources registered dynamically; available only for the lifetime of the session) +- 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 Documents: `demo://resource/static/document/` (serves files from `src/everything/docs/` as static file-based resources) +- Session Scoped: `demo://resource/session/` (per-session resources registered dynamically; available only for the lifetime of the session) ## Resource Subscriptions and Notifications -- Simulated update notifications are opt‑in and off by default. -- Clients may subscribe/unsubscribe to resource URIs using the MCP `resources/subscribe` and `resources/unsubscribe` requests. -- Use the `toggle-subscriber-updates` tool to start/stop a per‑session interval that emits `notifications/resources/updated { uri }` only for URIs that session has subscribed to. -- Multiple concurrent clients are supported; each client’s subscriptions are tracked per session and notifications are delivered independently via the server instance associated with that session. +- Simulated update notifications are opt‑in and off by default. +- Clients may subscribe/unsubscribe to resource URIs using the MCP `resources/subscribe` and `resources/unsubscribe` requests. +- Use the `toggle-subscriber-updates` tool to start/stop a per‑session interval that emits `notifications/resources/updated { uri }` only for URIs that session has subscribed to. +- Multiple concurrent clients are supported; each client’s subscriptions are tracked per session and notifications are delivered independently via the server instance associated with that session. ## Simulated Logging -- Simulated logging is available but off by default. -- Use the `toggle-simulated-logging` tool to start/stop periodic log messages of varying levels (debug, info, notice, warning, error, critical, alert, emergency) per session. -- Clients can control the minimum level they receive via the standard MCP `logging/setLevel` request. +- Simulated logging is available but off by default. +- Use the `toggle-simulated-logging` tool to start/stop periodic log messages of varying levels (debug, info, notice, warning, error, critical, alert, emergency) per session. +- Clients can control the minimum level they receive via the standard MCP `logging/setLevel` request. ## Tasks (SEP-1686) The server advertises support for MCP Tasks, enabling long-running operations with status tracking: -- **Capabilities advertised**: `tasks.list`, `tasks.cancel`, `tasks.requests.tools.call` -- **Task Store**: Uses `InMemoryTaskStore` from SDK experimental for task lifecycle management -- **Message Queue**: Uses `InMemoryTaskMessageQueue` for task-related messaging +- **Capabilities advertised**: `tasks.list`, `tasks.cancel`, `tasks.requests.tools.call` +- **Task Store**: Uses `InMemoryTaskStore` from SDK experimental for task lifecycle management +- **Message Queue**: Uses `InMemoryTaskMessageQueue` for task-related messaging ### Task Lifecycle @@ -73,11 +73,11 @@ The server advertises support for MCP Tasks, enabling long-running operations wi ### Task Statuses -- `working`: Task is actively processing -- `input_required`: Task needs additional input (server sends elicitation request directly) -- `completed`: Task finished successfully -- `failed`: Task encountered an error -- `cancelled`: Task was cancelled by client +- `working`: Task is actively processing +- `input_required`: Task needs additional input (server sends elicitation request directly) +- `completed`: Task finished successfully +- `failed`: Task encountered an error +- `cancelled`: Task was cancelled by client ### Demo Tools diff --git a/src/everything/docs/how-it-works.md b/src/everything/docs/how-it-works.md index af77cde7ff..514c6f5663 100644 --- a/src/everything/docs/how-it-works.md +++ b/src/everything/docs/how-it-works.md @@ -11,35 +11,35 @@ ### Module: `server/index.ts` -- Some tools require client support for the capability they demonstrate. These are: - - `get-roots-list` - - `trigger-elicitation-request` - - `trigger-sampling-request` -- Client capabilities aren't known until after initilization handshake is complete. -- Most tools are registered immediately during the Server Factory execution, prior to client connection. -- To defer registration of these commands until client capabilities are known, a `registerConditionalTools(server)` function is invoked from an `onintitialized` handler. +- Some tools require client support for the capability they demonstrate. These are: + - `get-roots-list` + - `trigger-elicitation-request` + - `trigger-sampling-request` +- Client capabilities aren't known until after initilization handshake is complete. +- Most tools are registered immediately during the Server Factory execution, prior to client connection. +- To defer registration of these commands until client capabilities are known, a `registerConditionalTools(server)` function is invoked from an `onintitialized` handler. ## Resource Subscriptions ### Module: `resources/subscriptions.ts` -- Tracks subscribers per URI: `Map>`. -- Installs handlers via `setSubscriptionHandlers(server)` to process subscribe/unsubscribe requests and keep the map updated. -- Updates are started/stopped on demand by the `toggle-subscriber-updates` tool, which calls `beginSimulatedResourceUpdates(server, sessionId)` and `stopSimulatedResourceUpdates(sessionId)`. -- `cleanup(sessionId?)` calls `stopSimulatedResourceUpdates(sessionId)` to clear intervals and remove session‑scoped state. +- Tracks subscribers per URI: `Map>`. +- Installs handlers via `setSubscriptionHandlers(server)` to process subscribe/unsubscribe requests and keep the map updated. +- Updates are started/stopped on demand by the `toggle-subscriber-updates` tool, which calls `beginSimulatedResourceUpdates(server, sessionId)` and `stopSimulatedResourceUpdates(sessionId)`. +- `cleanup(sessionId?)` calls `stopSimulatedResourceUpdates(sessionId)` to clear intervals and remove session‑scoped state. ## Session‑scoped Resources ### Module: `resources/session.ts` -- `getSessionResourceURI(name: string)`: Builds a session resource URI: `demo://resource/session/`. -- `registerSessionResource(server, resource, type, payload)`: Registers a resource with the given `uri`, `name`, and `mimeType`, returning a `resource_link`. The content is served from memory for the life of the session only. Supports `type: "text" | "blob"` and returns data in the corresponding field. -- Intended usage: tools can create and expose per-session artifacts without persisting them. For example, `tools/gzip-file-as-resource.ts` compresses fetched content, registers it as a session resource with `mimeType: application/gzip`, and returns either a `resource_link` or an inline `resource` based on `outputType`. +- `getSessionResourceURI(name: string)`: Builds a session resource URI: `demo://resource/session/`. +- `registerSessionResource(server, resource, type, payload)`: Registers a resource with the given `uri`, `name`, and `mimeType`, returning a `resource_link`. The content is served from memory for the life of the session only. Supports `type: "text" | "blob"` and returns data in the corresponding field. +- Intended usage: tools can create and expose per-session artifacts without persisting them. For example, `tools/gzip-file-as-resource.ts` compresses fetched content, registers it as a session resource with `mimeType: application/gzip`, and returns either a `resource_link` or an inline `resource` based on `outputType`. ## Simulated Logging ### Module: `server/logging.ts` -- Periodically sends randomized log messages at different levels. Messages can include the session ID for clarity during demos. -- Started/stopped on demand via the `toggle-simulated-logging` tool, which calls `beginSimulatedLogging(server, sessionId?)` and `stopSimulatedLogging(sessionId?)`. Note that transport disconnect triggers `cleanup()` which also stops any active intervals. -- Uses `server.sendLoggingMessage({ level, data }, sessionId?)` so that the client’s configured minimum logging level is respected by the SDK. +- Periodically sends randomized log messages at different levels. Messages can include the session ID for clarity during demos. +- Started/stopped on demand via the `toggle-simulated-logging` tool, which calls `beginSimulatedLogging(server, sessionId?)` and `stopSimulatedLogging(sessionId?)`. Note that transport disconnect triggers `cleanup()` which also stops any active intervals. +- Uses `server.sendLoggingMessage({ level, data }, sessionId?)` so that the client’s configured minimum logging level is respected by the SDK. diff --git a/src/everything/docs/instructions.md b/src/everything/docs/instructions.md index d0f32d5469..5806dc0ba9 100644 --- a/src/everything/docs/instructions.md +++ b/src/everything/docs/instructions.md @@ -5,23 +5,23 @@ Follow them to use, extend, and troubleshoot the server safely and effectively. ## Cross-Feature Relationships -- Use `get-roots-list` to see client workspace roots before file operations -- `gzip-file-as-resource` creates session-scoped resources accessible only during the current session -- Enable `toggle-simulated-logging` before debugging to see server log messages -- Enable `toggle-subscriber-updates` to receive periodic resource update notifications +- Use `get-roots-list` to see client workspace roots before file operations +- `gzip-file-as-resource` creates session-scoped resources accessible only during the current session +- Enable `toggle-simulated-logging` before debugging to see server log messages +- Enable `toggle-subscriber-updates` to receive periodic resource update notifications ## Constraints & Limitations -- `gzip-file-as-resource`: Max fetch size controlled by `GZIP_MAX_FETCH_SIZE` (default 10MB), timeout by `GZIP_MAX_FETCH_TIME_MILLIS` (default 30s), allowed domains by `GZIP_ALLOWED_DOMAINS` -- Session resources are ephemeral and lost when the session ends -- Sampling requests (`trigger-sampling-request`) require client sampling capability -- Elicitation requests (`trigger-elicitation-request`) require client elicitation capability +- `gzip-file-as-resource`: Max fetch size controlled by `GZIP_MAX_FETCH_SIZE` (default 10MB), timeout by `GZIP_MAX_FETCH_TIME_MILLIS` (default 30s), allowed domains by `GZIP_ALLOWED_DOMAINS` +- Session resources are ephemeral and lost when the session ends +- Sampling requests (`trigger-sampling-request`) require client sampling capability +- Elicitation requests (`trigger-elicitation-request`) require client elicitation capability ## Operational Patterns -- For long operations, use `trigger-long-running-operation` which sends progress notifications -- Prefer reading resources before calling mutating tools -- Check `get-roots-list` output to understand the client's workspace context +- For long operations, use `trigger-long-running-operation` which sends progress notifications +- Prefer reading resources before calling mutating tools +- Check `get-roots-list` output to understand the client's workspace context ## Easter Egg diff --git a/src/everything/docs/startup.md b/src/everything/docs/startup.md index bd2f02a6ab..1d006589a9 100644 --- a/src/everything/docs/startup.md +++ b/src/everything/docs/startup.md @@ -9,63 +9,63 @@ ## 1. Everything Server Launcher -- Usage `node dist/index.js [stdio|sse|streamableHttp]` -- Runs the specified **transport manager** to handle client connections. -- Specify transport type on command line (default `stdio`) - - `stdio` → `transports/stdio.js` - - `sse` → `transports/sse.js` - - `streamableHttp` → `transports/streamableHttp.js` +- Usage `node dist/index.js [stdio|sse|streamableHttp]` +- Runs the specified **transport manager** to handle client connections. +- Specify transport type on command line (default `stdio`) + - `stdio` → `transports/stdio.js` + - `sse` → `transports/sse.js` + - `streamableHttp` → `transports/streamableHttp.js` ## 2. The Transport Manager -- Creates a server instance using `createServer()` from `server/index.ts` - - Connects it to the chosen transport type from the MCP SDK. -- Handles communication according to the MCP specs for the chosen transport. - - **STDIO**: - - One simple, process‑bound connection. - - Calls`clientConnect()` upon connection. - - Closes and calls `cleanup()` on `SIGINT`. - - **SSE**: - - Supports multiple client connections. - - Client transports are mapped to `sessionId`; - - Calls `clientConnect(sessionId)` upon connection. - - Hooks server’s `onclose` to clean and remove session. - - Exposes - - `/sse` **GET** (SSE stream) - - `/message` **POST** (JSON‑RPC messages) - - **Streamable HTTP**: - - Supports multiple client connections. - - Client transports are mapped to `sessionId`; - - Calls `clientConnect(sessionId)` upon connection. - - Exposes `/mcp` for - - **POST** (JSON‑RPC messages) - - **GET** (SSE stream) - - **DELETE** (termination) - - Uses an event store for resumability and stores transports by `sessionId`. - - Calls `cleanup(sessionId)` on **DELETE**. +- Creates a server instance using `createServer()` from `server/index.ts` + - Connects it to the chosen transport type from the MCP SDK. +- Handles communication according to the MCP specs for the chosen transport. + - **STDIO**: + - One simple, process‑bound connection. + - Calls`clientConnect()` upon connection. + - Closes and calls `cleanup()` on `SIGINT`. + - **SSE**: + - Supports multiple client connections. + - Client transports are mapped to `sessionId`; + - Calls `clientConnect(sessionId)` upon connection. + - Hooks server’s `onclose` to clean and remove session. + - Exposes + - `/sse` **GET** (SSE stream) + - `/message` **POST** (JSON‑RPC messages) + - **Streamable HTTP**: + - Supports multiple client connections. + - Client transports are mapped to `sessionId`; + - Calls `clientConnect(sessionId)` upon connection. + - Exposes `/mcp` for + - **POST** (JSON‑RPC messages) + - **GET** (SSE stream) + - **DELETE** (termination) + - Uses an event store for resumability and stores transports by `sessionId`. + - Calls `cleanup(sessionId)` on **DELETE**. ## 3. The Server Factory -- Invoke `createServer()` from `server/index.ts` -- Creates a new `McpServer` instance with - - **Capabilities**: - - `tools: {}` - - `logging: {}` - - `prompts: {}` - - `resources: { subscribe: true }` - - **Server Instructions** - - Loaded from the docs folder (`server-instructions.md`). - - **Registrations** - - Registers **tools** via `registerTools(server)`. - - Registers **resources** via `registerResources(server)`. - - Registers **prompts** via `registerPrompts(server)`. - - **Other Request Handlers** - - Sets up resource subscription handlers via `setSubscriptionHandlers(server)`. - - Roots list change handler is added post-connection via - - **Returns** - - The `McpServer` instance - - A `clientConnect(sessionId)` callback that enables post-connection setup - - A `cleanup(sessionId?)` callback that stops any active intervals and removes any session‑scoped state +- Invoke `createServer()` from `server/index.ts` +- Creates a new `McpServer` instance with + - **Capabilities**: + - `tools: {}` + - `logging: {}` + - `prompts: {}` + - `resources: { subscribe: true }` + - **Server Instructions** + - Loaded from the docs folder (`server-instructions.md`). + - **Registrations** + - Registers **tools** via `registerTools(server)`. + - Registers **resources** via `registerResources(server)`. + - Registers **prompts** via `registerPrompts(server)`. + - **Other Request Handlers** + - Sets up resource subscription handlers via `setSubscriptionHandlers(server)`. + - Roots list change handler is added post-connection via + - **Returns** + - The `McpServer` instance + - A `clientConnect(sessionId)` callback that enables post-connection setup + - A `cleanup(sessionId?)` callback that stops any active intervals and removes any session‑scoped state ## Enabling Multiple Clients diff --git a/src/everything/docs/structure.md b/src/everything/docs/structure.md index eeb343eb02..bd3d70b95c 100644 --- a/src/everything/docs/structure.md +++ b/src/everything/docs/structure.md @@ -69,126 +69,126 @@ src/everything ### `index.ts` -- CLI entry point that selects and runs a specific transport module based on the first CLI argument: `stdio`, `sse`, or `streamableHttp`. +- CLI entry point that selects and runs a specific transport module based on the first CLI argument: `stdio`, `sse`, or `streamableHttp`. ### `AGENTS.md` -- Directions for Agents/LLMs explaining coding guidelines and how to appropriately extend the server. +- Directions for Agents/LLMs explaining coding guidelines and how to appropriately extend the server. ### `package.json` -- Package metadata and scripts: - - `build`: TypeScript compile to `dist/`, copies `docs/` into `dist/` and marks the compiled entry scripts as executable. - - `start:stdio`, `start:sse`, `start:streamableHttp`: Run built transports from `dist/`. -- Declares dependencies on `@modelcontextprotocol/sdk`, `express`, `cors`, `zod`, etc. +- Package metadata and scripts: + - `build`: TypeScript compile to `dist/`, copies `docs/` into `dist/` and marks the compiled entry scripts as executable. + - `start:stdio`, `start:sse`, `start:streamableHttp`: Run built transports from `dist/`. +- Declares dependencies on `@modelcontextprotocol/sdk`, `express`, `cors`, `zod`, etc. ### `docs/` -- `architecture.md` - - This document. -- `instructions.md` - - Human‑readable instructions intended to be passed to the client/LLM as guidance on server use. Loaded by the server at startup and returned in the initialize exchange. +- `architecture.md` + - This document. +- `instructions.md` + - Human‑readable instructions intended to be passed to the client/LLM as guidance on server use. Loaded by the server at startup and returned in the initialize exchange. ### `prompts/` -- `index.ts` - - `registerPrompts(server)` orchestrator; delegates to prompt factory/registration methods from in individual prompt files. -- `simple.ts` - - Registers `simple-prompt`: a prompt with no arguments that returns a single user 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 `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`. +- `index.ts` + - `registerPrompts(server)` orchestrator; delegates to prompt factory/registration methods from in individual prompt files. +- `simple.ts` + - Registers `simple-prompt`: a prompt with no arguments that returns a single user 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 `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 resource factory/registration methods from individual resource files. -- `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 positive integer. Content is generated on demand with a timestamp. - - 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`. +- `index.ts` + - `registerResources(server)` orchestrator; delegates to resource factory/registration methods from individual resource files. +- `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 positive integer. Content is generated on demand with a timestamp. + - 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`. ### `server/` -- `index.ts` - - Server factory that creates an `McpServer` with declared capabilities, loads server instructions, and registers tools, prompts, and resources. - - Sets resource subscription handlers via `setSubscriptionHandlers(server)`. - - Exposes `{ server, cleanup }` to the chosen transport. Cleanup stops any running intervals in the server when the transport disconnects. -- `logging.ts` - - Implements simulated logging. Periodically sends randomized log messages at various levels to the connected client session. Started/stopped on demand via a dedicated tool. +- `index.ts` + - Server factory that creates an `McpServer` with declared capabilities, loads server instructions, and registers tools, prompts, and resources. + - Sets resource subscription handlers via `setSubscriptionHandlers(server)`. + - Exposes `{ server, cleanup }` to the chosen transport. Cleanup stops any running intervals in the server when the transport disconnects. +- `logging.ts` + - Implements simulated logging. Periodically sends randomized log messages at various levels to the connected client session. Started/stopped on demand via a dedicated tool. ### `tools/` -- `index.ts` - - `registerTools(server)` orchestrator; delegates to tool factory/registration methods in individual tool files. -- `echo.ts` - - Registers an `echo` tool that takes a message and returns `Echo: {message}`. -- `get-annotated-message.ts` - - Registers a `get-annotated-message` tool which demonstrates content-level annotations. Emits a primary `text` message with content `annotations` (`priority`, `audience`) that vary by `messageType` (`"error" | "success" | "debug"`), and optionally includes an annotated `image` (tiny PNG) when `includeImage` is true. All tools in this server include tool-level annotations (`readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`). -- `get-env.ts` - - Registers a `get-env` tool that returns the current process environment variables as formatted JSON text; useful for debugging configuration. -- `get-resource-links.ts` - - Registers a `get-resource-links` tool that returns an intro `text` block followed by multiple `resource_link` items. -- `get-resource-reference.ts` - - Registers a `get-resource-reference` tool that returns a reference for a selected dynamic resource. -- `get-roots-list.ts` - - Registers a `get-roots-list` tool that returns the last list of roots sent by the client. -- `gzip-file-as-resource.ts` - - Registers a `gzip-file-as-resource` tool that fetches content from a URL or data URI, compresses it, and then either: - - returns a `resource_link` to a session-scoped resource (default), or - - returns an inline `resource` with the gzipped data. The resource will be still discoverable for the duration of the session via `resources/list`. - - Uses `resources/session.ts` to register the gzipped blob as a per-session resource at a URI like `demo://resource/session/` with `mimeType: application/gzip`. - - Environment controls: - - `GZIP_MAX_FETCH_SIZE` (bytes, default 10 MiB) - - `GZIP_MAX_FETCH_TIME_MILLIS` (ms, default 30000) - - `GZIP_ALLOWED_DOMAINS` (comma-separated allowlist; empty means all domains allowed) -- `simulate-research-query.ts` - - Registers a `simulate-research-query` task-based tool that demonstrates the MCP Tasks feature (SEP-1686). Simulates a multi-stage research operation with progress updates. If the query is marked as ambiguous and the client supports elicitation, it pauses mid-execution to request clarification via `elicitation/create`. Uses `server.experimental.tasks.registerToolTask()` with `execution: { taskSupport: "required" }`. -- `trigger-elicitation-request.ts` - - Registers a `trigger-elicitation-request` tool that sends an `elicitation/create` request to the client/LLM and returns the elicitation result. -- `trigger-url-elicitation.ts` - - Registers a `trigger-url-elicitation` tool that either sends an out-of-band URL-mode `elicitation/create` request (`mode: "url"`) including an `elicitationId` (request path) or throws `UrlElicitationRequiredError` (`-32042`) for client-handled URL elicitation (error path). On the error path the carried prerequisite elicitation points at a different URL than the failing one (`https://modelcontextprotocol.io`), and when the client satisfies it and retries the same call, the retry ignores `errorPath` and proceeds via the request path — so the client does not loop on the same error. -- `trigger-elicitation-request-async.ts` - - Registers a `trigger-elicitation-request-async` tool that demonstrates bidirectional MCP tasks for elicitation. Sends an elicitation request with task metadata, then polls the client's `tasks/get` endpoint for completion status before fetching the final result. -- `trigger-sampling-request.ts` - - Registers a `trigger-sampling-request` tool that sends a `sampling/createMessage` request to the client/LLM and returns the sampling result. -- `trigger-sampling-request-async.ts` - - Registers a `trigger-sampling-request-async` tool that demonstrates bidirectional MCP tasks for sampling. Sends a sampling request with task metadata, then polls the client's `tasks/get` endpoint for completion status before fetching the final result. -- `get-structured-content.ts` - - Registers a `get-structured-content` tool that demonstrates structuredContent block responses. -- `get-sum.ts` - - Registers a `get-sum` tool with a Zod input schema that sums two numbers `a` and `b` and returns the result. -- `get-tiny-image.ts` - - Registers a `get-tiny-image` tool, which returns a tiny PNG MCP logo as an `image` content item, along with surrounding descriptive `text` items. -- `trigger-long-running-operation.ts` - - Registers a `trigger-long-running-operation` tool that simulates a long-running task over a specified `duration` (seconds) and number of `steps`; emits `notifications/progress` updates when the client supplies a `progressToken`. -- `toggle-simulated-logging.ts` - - Registers a `toggle-simulated-logging` tool, which starts or stops simulated logging for the invoking session. -- `toggle-subscriber-updates.ts` - - Registers a `toggle-subscriber-updates` tool, which starts or stops simulated resource subscription update checks for the invoking session. +- `index.ts` + - `registerTools(server)` orchestrator; delegates to tool factory/registration methods in individual tool files. +- `echo.ts` + - Registers an `echo` tool that takes a message and returns `Echo: {message}`. +- `get-annotated-message.ts` + - Registers a `get-annotated-message` tool which demonstrates content-level annotations. Emits a primary `text` message with content `annotations` (`priority`, `audience`) that vary by `messageType` (`"error" | "success" | "debug"`), and optionally includes an annotated `image` (tiny PNG) when `includeImage` is true. All tools in this server include tool-level annotations (`readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`). +- `get-env.ts` + - Registers a `get-env` tool that returns the current process environment variables as formatted JSON text; useful for debugging configuration. +- `get-resource-links.ts` + - Registers a `get-resource-links` tool that returns an intro `text` block followed by multiple `resource_link` items. +- `get-resource-reference.ts` + - Registers a `get-resource-reference` tool that returns a reference for a selected dynamic resource. +- `get-roots-list.ts` + - Registers a `get-roots-list` tool that returns the last list of roots sent by the client. +- `gzip-file-as-resource.ts` + - Registers a `gzip-file-as-resource` tool that fetches content from a URL or data URI, compresses it, and then either: + - returns a `resource_link` to a session-scoped resource (default), or + - returns an inline `resource` with the gzipped data. The resource will be still discoverable for the duration of the session via `resources/list`. + - Uses `resources/session.ts` to register the gzipped blob as a per-session resource at a URI like `demo://resource/session/` with `mimeType: application/gzip`. + - Environment controls: + - `GZIP_MAX_FETCH_SIZE` (bytes, default 10 MiB) + - `GZIP_MAX_FETCH_TIME_MILLIS` (ms, default 30000) + - `GZIP_ALLOWED_DOMAINS` (comma-separated allowlist; empty means all domains allowed) +- `simulate-research-query.ts` + - Registers a `simulate-research-query` task-based tool that demonstrates the MCP Tasks feature (SEP-1686). Simulates a multi-stage research operation with progress updates. If the query is marked as ambiguous and the client supports elicitation, it pauses mid-execution to request clarification via `elicitation/create`. Uses `server.experimental.tasks.registerToolTask()` with `execution: { taskSupport: "required" }`. +- `trigger-elicitation-request.ts` + - Registers a `trigger-elicitation-request` tool that sends an `elicitation/create` request to the client/LLM and returns the elicitation result. +- `trigger-url-elicitation.ts` + - Registers a `trigger-url-elicitation` tool that either sends an out-of-band URL-mode `elicitation/create` request (`mode: "url"`) including an `elicitationId` (request path) or throws `UrlElicitationRequiredError` (`-32042`) for client-handled URL elicitation (error path). On the error path the carried prerequisite elicitation points at a different URL than the failing one (`https://modelcontextprotocol.io`), and when the client satisfies it and retries the same call, the retry ignores `errorPath` and proceeds via the request path — so the client does not loop on the same error. +- `trigger-elicitation-request-async.ts` + - Registers a `trigger-elicitation-request-async` tool that demonstrates bidirectional MCP tasks for elicitation. Sends an elicitation request with task metadata, then polls the client's `tasks/get` endpoint for completion status before fetching the final result. +- `trigger-sampling-request.ts` + - Registers a `trigger-sampling-request` tool that sends a `sampling/createMessage` request to the client/LLM and returns the sampling result. +- `trigger-sampling-request-async.ts` + - Registers a `trigger-sampling-request-async` tool that demonstrates bidirectional MCP tasks for sampling. Sends a sampling request with task metadata, then polls the client's `tasks/get` endpoint for completion status before fetching the final result. +- `get-structured-content.ts` + - Registers a `get-structured-content` tool that demonstrates structuredContent block responses. +- `get-sum.ts` + - Registers a `get-sum` tool with a Zod input schema that sums two numbers `a` and `b` and returns the result. +- `get-tiny-image.ts` + - Registers a `get-tiny-image` tool, which returns a tiny PNG MCP logo as an `image` content item, along with surrounding descriptive `text` items. +- `trigger-long-running-operation.ts` + - Registers a `trigger-long-running-operation` tool that simulates a long-running task over a specified `duration` (seconds) and number of `steps`; emits `notifications/progress` updates when the client supplies a `progressToken`. +- `toggle-simulated-logging.ts` + - Registers a `toggle-simulated-logging` tool, which starts or stops simulated logging for the invoking session. +- `toggle-subscriber-updates.ts` + - Registers a `toggle-subscriber-updates` tool, which starts or stops simulated resource subscription update checks for the invoking session. ### `transports/` -- `stdio.ts` - - Starts a `StdioServerTransport`, created the server via `createServer()`, and connects it. - - Handles `SIGINT` to close cleanly and calls `cleanup()` to remove any live intervals. -- `sse.ts` - - Express server exposing: - - `GET /sse` to establish an SSE connection per session. - - `POST /message` for client messages. - - Manages multiple connected clients via a transport map. - - Starts an `SSEServerTransport`, created the server via `createServer()`, and connects it to a new transport. - - On server disconnect, calls `cleanup()` to remove any live intervals. -- `streamableHttp.ts` - - Express server exposing a single `/mcp` endpoint for POST (JSON‑RPC), GET (SSE stream), and DELETE (session termination) using `StreamableHTTPServerTransport`. - - Uses an `InMemoryEventStore` for resumable sessions and tracks transports by `sessionId`. - - Connects a fresh server instance on initialization POST and reuses the transport for subsequent requests. +- `stdio.ts` + - Starts a `StdioServerTransport`, created the server via `createServer()`, and connects it. + - Handles `SIGINT` to close cleanly and calls `cleanup()` to remove any live intervals. +- `sse.ts` + - Express server exposing: + - `GET /sse` to establish an SSE connection per session. + - `POST /message` for client messages. + - Manages multiple connected clients via a transport map. + - Starts an `SSEServerTransport`, created the server via `createServer()`, and connects it to a new transport. + - On server disconnect, calls `cleanup()` to remove any live intervals. +- `streamableHttp.ts` + - Express server exposing a single `/mcp` endpoint for POST (JSON‑RPC), GET (SSE stream), and DELETE (session termination) using `StreamableHTTPServerTransport`. + - Uses an `InMemoryEventStore` for resumable sessions and tracks transports by `sessionId`. + - Connects a fresh server instance on initialization POST and reuses the transport for subsequent requests. diff --git a/src/everything/index.ts b/src/everything/index.ts index f591211535..39d50fa651 100644 --- a/src/everything/index.ts +++ b/src/everything/index.ts @@ -2,41 +2,41 @@ // Parse command line arguments first const args = process.argv.slice(2); -const scriptName = args[0] || 'stdio'; +const scriptName = args[0] || "stdio"; async function run() { - try { - // Dynamically import only the requested module to prevent all modules from initializing - switch (scriptName) { - case 'stdio': - // Import and run the default server - await import('./transports/stdio.js'); - break; - case 'sse': - // Import and run the SSE server - await import('./transports/sse.js'); - break; - case 'streamableHttp': - // Import and run the streamable HTTP server - await import('./transports/streamableHttp.js'); - break; - default: - console.error(`-`.repeat(53)); - console.error(` Everything Server Launcher`); - console.error(` Usage: node ./index.js [stdio|sse|streamableHttp]`); - console.error(` Default transport: stdio`); - console.error(`-`.repeat(53)); - console.error(`Unknown transport: ${scriptName}`); - console.log('Available transports:'); - console.log('- stdio'); - console.log('- sse'); - console.log('- streamableHttp'); - process.exit(1); - } - } catch (error) { - console.error('Error running script:', error); + try { + // Dynamically import only the requested module to prevent all modules from initializing + switch (scriptName) { + case "stdio": + // Import and run the default server + await import("./transports/stdio.js"); + break; + case "sse": + // Import and run the SSE server + await import("./transports/sse.js"); + break; + case "streamableHttp": + // Import and run the streamable HTTP server + await import("./transports/streamableHttp.js"); + break; + default: + console.error(`-`.repeat(53)); + console.error(` Everything Server Launcher`); + console.error(` Usage: node ./index.js [stdio|sse|streamableHttp]`); + console.error(` Default transport: stdio`); + console.error(`-`.repeat(53)); + console.error(`Unknown transport: ${scriptName}`); + console.log("Available transports:"); + console.log("- stdio"); + console.log("- sse"); + console.log("- streamableHttp"); process.exit(1); } + } catch (error) { + console.error("Error running script:", error); + process.exit(1); + } } await run(); diff --git a/src/everything/package.json b/src/everything/package.json index 614d865b06..acbc498a21 100644 --- a/src/everything/package.json +++ b/src/everything/package.json @@ -1,51 +1,51 @@ { - "name": "@modelcontextprotocol/server-everything", - "version": "2.0.0", - "description": "MCP server that exercises all the features of the MCP protocol", - "license": "SEE LICENSE IN LICENSE", - "mcpName": "io.github.modelcontextprotocol/server-everything", - "author": "Model Context Protocol a Series of LF Projects, LLC.", - "homepage": "https://modelcontextprotocol.io", - "bugs": "https://github.com/modelcontextprotocol/servers/issues", - "repository": { - "type": "git", - "url": "https://github.com/modelcontextprotocol/servers.git" - }, - "type": "module", - "bin": { - "mcp-server-everything": "dist/index.js" - }, - "files": [ - "dist" - ], - "scripts": { - "build": "tsc && shx cp -r docs dist/ && shx chmod +x dist/*.js", - "prepare": "npm run build", - "watch": "tsc --watch", - "start:stdio": "node dist/index.js stdio", - "start:sse": "node dist/index.js sse", - "start:streamableHttp": "node dist/index.js streamableHttp", - "prettier:fix": "prettier --write .", - "prettier:check": "prettier --check .", - "test": "vitest run --coverage" - }, - "dependencies": { - "cors": "^2.8.5", - "express": "^5.2.1", - "jszip": "^3.10.1", - "zod": "^4.0.0", - "@modelcontextprotocol/server": "^2.0.0-alpha.3", - "@modelcontextprotocol/core": "^2.0.0-alpha.1", - "@modelcontextprotocol/server-legacy": "^2.0.0-alpha.3", - "@modelcontextprotocol/node": "^2.0.0-alpha.3" - }, - "devDependencies": { - "@types/cors": "^2.8.19", - "@types/express": "^5.0.6", - "@vitest/coverage-v8": "^4.1.8", - "prettier": "^2.8.8", - "shx": "^0.3.4", - "typescript": "^5.6.2", - "vitest": "^4.1.8" - } + "name": "@modelcontextprotocol/server-everything", + "version": "2.0.0", + "description": "MCP server that exercises all the features of the MCP protocol", + "license": "SEE LICENSE IN LICENSE", + "mcpName": "io.github.modelcontextprotocol/server-everything", + "author": "Model Context Protocol a Series of LF Projects, LLC.", + "homepage": "https://modelcontextprotocol.io", + "bugs": "https://github.com/modelcontextprotocol/servers/issues", + "repository": { + "type": "git", + "url": "https://github.com/modelcontextprotocol/servers.git" + }, + "type": "module", + "bin": { + "mcp-server-everything": "dist/index.js" + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsc && shx cp -r docs dist/ && shx chmod +x dist/*.js", + "prepare": "npm run build", + "watch": "tsc --watch", + "start:stdio": "node dist/index.js stdio", + "start:sse": "node dist/index.js sse", + "start:streamableHttp": "node dist/index.js streamableHttp", + "prettier:fix": "prettier --write .", + "prettier:check": "prettier --check .", + "test": "vitest run --coverage" + }, + "dependencies": { + "cors": "^2.8.5", + "express": "^5.2.1", + "jszip": "^3.10.1", + "zod": "^4.0.0", + "@modelcontextprotocol/server": "^2.0.0-alpha.3", + "@modelcontextprotocol/core": "^2.0.0-alpha.1", + "@modelcontextprotocol/server-legacy": "^2.0.0-alpha.3", + "@modelcontextprotocol/node": "^2.0.0-alpha.3" + }, + "devDependencies": { + "@types/cors": "^2.8.19", + "@types/express": "^5.0.6", + "@vitest/coverage-v8": "^4.1.8", + "prettier": "^2.8.8", + "shx": "^0.3.4", + "typescript": "^5.6.2", + "vitest": "^4.1.8" + } } diff --git a/src/everything/prompts/args.ts b/src/everything/prompts/args.ts index 65f1d9bba1..b67b72790c 100644 --- a/src/everything/prompts/args.ts +++ b/src/everything/prompts/args.ts @@ -1,5 +1,5 @@ -import { z } from 'zod'; -import { McpServer } from '@modelcontextprotocol/server'; +import { z } from "zod"; +import { McpServer } from "@modelcontextprotocol/server"; /** * Register a prompt with arguments @@ -9,34 +9,34 @@ import { McpServer } from '@modelcontextprotocol/server'; * @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() - }; + // Prompt arguments + const promptArgsSchema = { + city: z.string().describe("Name of the city"), + state: z.string().describe("Name of the state").optional(), + }; - // Register the prompt - /* @mcp-codemod-error Could not verify `argsSchema` is a schema object. Raw shapes are deprecated in v2 — pass a Standard Schema object (e.g. z.object({ … })); no change is needed if it already is one. */ - server.registerPrompt( - 'args-prompt', - { - title: 'Arguments Prompt', - description: 'A prompt with two arguments, one required and one optional', - argsSchema: promptArgsSchema - }, - args => { - const location = `${args?.city}${args?.state ? `, ${args?.state}` : ''}`; - return { - messages: [ - { - role: 'user', - content: { - type: 'text', - text: `What's weather in ${location}?` - } - } - ] - }; - } - ); + // Register the prompt + /* @mcp-codemod-error Could not verify `argsSchema` is a schema object. Raw shapes are deprecated in v2 — pass a Standard Schema object (e.g. z.object({ … })); no change is needed if it already is one. */ + server.registerPrompt( + "args-prompt", + { + title: "Arguments Prompt", + description: "A prompt with two arguments, one required and one optional", + argsSchema: promptArgsSchema, + }, + (args) => { + const location = `${args?.city}${args?.state ? `, ${args?.state}` : ""}`; + return { + messages: [ + { + role: "user", + content: { + type: "text", + text: `What's weather in ${location}?`, + }, + }, + ], + }; + } + ); }; diff --git a/src/everything/prompts/completions.ts b/src/everything/prompts/completions.ts index 8090f73ec3..d70c492ac3 100644 --- a/src/everything/prompts/completions.ts +++ b/src/everything/prompts/completions.ts @@ -1,5 +1,5 @@ -import { z } from 'zod'; -import { McpServer, completable } from '@modelcontextprotocol/server'; +import { z } from "zod"; +import { McpServer, completable } from "@modelcontextprotocol/server"; /** * Register a prompt with completable arguments @@ -10,45 +10,55 @@ import { McpServer, completable } from '@modelcontextprotocol/server'; * @param server */ export const registerPromptWithCompletions = (server: McpServer) => { - // Prompt arguments - const promptArgsSchema = { - department: completable(z.string().describe('Choose the department.'), value => { - return ['Engineering', 'Sales', 'Marketing', 'Support'].filter(d => d.startsWith(value)); - }), - 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 []; - }) - }; + // Prompt arguments + const promptArgsSchema = { + department: completable( + z.string().describe("Choose the department."), + (value) => { + return ["Engineering", "Sales", "Marketing", "Support"].filter((d) => + d.startsWith(value) + ); + } + ), + 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 - /* @mcp-codemod-error Could not verify `argsSchema` is a schema object. Raw shapes are deprecated in v2 — pass a Standard Schema object (e.g. z.object({ … })); no change is needed if it already is one. */ - server.registerPrompt( - 'completable-prompt', + // Register the prompt + /* @mcp-codemod-error Could not verify `argsSchema` is a schema object. Raw shapes are deprecated in v2 — pass a Standard Schema object (e.g. z.object({ … })); no change is needed if it already is one. */ + server.registerPrompt( + "completable-prompt", + { + title: "Team Management", + description: "First argument choice narrows values for second argument.", + argsSchema: promptArgsSchema, + }, + ({ department, name }) => ({ + messages: [ { - title: 'Team Management', - description: 'First argument choice narrows values for second argument.', - argsSchema: promptArgsSchema + role: "user", + content: { + type: "text", + text: `Please promote ${name} to the head of the ${department} team.`, + }, }, - ({ department, name }) => ({ - messages: [ - { - role: 'user', - content: { - type: 'text', - text: `Please promote ${name} to the head of the ${department} team.` - } - } - ] - }) - ); + ], + }) + ); }; diff --git a/src/everything/prompts/index.ts b/src/everything/prompts/index.ts index e70c890add..22d6a22baf 100644 --- a/src/everything/prompts/index.ts +++ b/src/everything/prompts/index.ts @@ -1,8 +1,8 @@ -import { McpServer } from '@modelcontextprotocol/server'; -import { registerSimplePrompt } from './simple.js'; -import { registerArgumentsPrompt } from './args.js'; -import { registerPromptWithCompletions } from './completions.js'; -import { registerEmbeddedResourcePrompt } from './resource.js'; +import { McpServer } from "@modelcontextprotocol/server"; +import { registerSimplePrompt } from "./simple.js"; +import { registerArgumentsPrompt } from "./args.js"; +import { registerPromptWithCompletions } from "./completions.js"; +import { registerEmbeddedResourcePrompt } from "./resource.js"; /** * Register the prompts with the MCP server. @@ -10,8 +10,8 @@ import { registerEmbeddedResourcePrompt } from './resource.js'; * @param server */ export const registerPrompts = (server: McpServer) => { - registerSimplePrompt(server); - registerArgumentsPrompt(server); - registerPromptWithCompletions(server); - registerEmbeddedResourcePrompt(server); + registerSimplePrompt(server); + registerArgumentsPrompt(server); + registerPromptWithCompletions(server); + registerEmbeddedResourcePrompt(server); }; diff --git a/src/everything/prompts/resource.ts b/src/everything/prompts/resource.ts index 9c4cc94f12..5d3c9690c6 100644 --- a/src/everything/prompts/resource.ts +++ b/src/everything/prompts/resource.ts @@ -1,14 +1,17 @@ -import { McpServer } from '@modelcontextprotocol/server'; -import { resourceTypeCompleter, resourceIdForPromptCompleter } from '../resources/templates.js'; +import { McpServer } from "@modelcontextprotocol/server"; import { - textResource, - textResourceUri, - blobResourceUri, - blobResource, - RESOURCE_TYPE_BLOB, - RESOURCE_TYPE_TEXT, - RESOURCE_TYPES -} from '../resources/templates.js'; + resourceTypeCompleter, + resourceIdForPromptCompleter, +} from "../resources/templates.js"; +import { + textResource, + textResourceUri, + blobResourceUri, + blobResource, + RESOURCE_TYPE_BLOB, + RESOURCE_TYPE_TEXT, + RESOURCE_TYPES, +} from "../resources/templates.js"; /** * Register a prompt with an embedded resource reference @@ -18,56 +21,74 @@ import { * @param server */ export const registerEmbeddedResourcePrompt = (server: McpServer) => { - // Prompt arguments - const promptArgsSchema = { - resourceType: resourceTypeCompleter, - resourceId: resourceIdForPromptCompleter - }; + // Prompt arguments + const promptArgsSchema = { + resourceType: resourceTypeCompleter, + resourceId: resourceIdForPromptCompleter, + }; - // Register the prompt - /* @mcp-codemod-error Could not verify `argsSchema` is a schema object. Raw shapes are deprecated in v2 — pass a Standard Schema object (e.g. z.object({ … })); no change is needed if it already is one. */ - server.registerPrompt( - 'resource-prompt', - { - title: 'Resource Prompt', - description: 'A prompt that includes an embedded resource reference', - argsSchema: promptArgsSchema - }, - args => { - // Validate resource type argument - 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 ${RESOURCE_TYPE_TEXT} or ${RESOURCE_TYPE_BLOB}.`); - } + // Register the prompt + /* @mcp-codemod-error Could not verify `argsSchema` is a schema object. Raw shapes are deprecated in v2 — pass a Standard Schema object (e.g. z.object({ … })); no change is needed if it already is one. */ + server.registerPrompt( + "resource-prompt", + { + title: "Resource Prompt", + description: "A prompt that includes an embedded resource reference", + argsSchema: promptArgsSchema, + }, + (args) => { + // Validate resource type argument + 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 ${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.`); - } + // 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); + // 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 { - messages: [ - { - role: 'user', - content: { - type: 'text', - text: `This prompt includes the ${resourceType} resource with id: ${resourceId}. Please analyze the following resource:` - } - }, - { - role: 'user', - content: { - type: 'resource', - resource: resource - } - } - ] - }; - } - ); + return { + messages: [ + { + role: "user", + content: { + type: "text", + text: `This prompt includes the ${resourceType} resource with id: ${resourceId}. Please analyze the following resource:`, + }, + }, + { + role: "user", + content: { + type: "resource", + resource: resource, + }, + }, + ], + }; + } + ); }; diff --git a/src/everything/prompts/simple.ts b/src/everything/prompts/simple.ts index 4e185aa038..8e2c5c67cd 100644 --- a/src/everything/prompts/simple.ts +++ b/src/everything/prompts/simple.ts @@ -1,4 +1,4 @@ -import { McpServer } from '@modelcontextprotocol/server'; +import { McpServer } from "@modelcontextprotocol/server"; /** * Register a simple prompt with no arguments @@ -7,23 +7,23 @@ import { McpServer } from '@modelcontextprotocol/server'; * @param server */ export const registerSimplePrompt = (server: McpServer) => { - // Register the prompt - server.registerPrompt( - 'simple-prompt', + // Register the prompt + server.registerPrompt( + "simple-prompt", + { + title: "Simple Prompt", + description: "A prompt with no arguments", + }, + () => ({ + messages: [ { - title: 'Simple Prompt', - description: 'A prompt with no arguments' + role: "user", + content: { + type: "text", + text: "This is a simple prompt without arguments.", + }, }, - () => ({ - messages: [ - { - role: 'user', - content: { - type: 'text', - text: 'This is a simple prompt without arguments.' - } - } - ] - }) - ); + ], + }) + ); }; diff --git a/src/everything/resources/files.ts b/src/everything/resources/files.ts index 3887e2024a..f4b9b5d7fc 100644 --- a/src/everything/resources/files.ts +++ b/src/everything/resources/files.ts @@ -1,7 +1,7 @@ -import { McpServer } from '@modelcontextprotocol/server'; -import { dirname, join } from 'path'; -import { fileURLToPath } from 'url'; -import { readdirSync, readFileSync, statSync } from 'fs'; +import { McpServer } from "@modelcontextprotocol/server"; +import { dirname, join } from "path"; +import { fileURLToPath } from "url"; +import { readdirSync, readFileSync, statSync } from "fs"; /** * Register static file resources @@ -14,48 +14,53 @@ import { readdirSync, readFileSync, statSync } from 'fs'; * @param server */ 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[] = []; + // 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 + return; + } + + // Register each file as a static resource + for (const name of entries) { + // Only process files, not directories + const fullPath = join(docsDir, name); try { - entries = readdirSync(docsDir); - } catch (e) { - // If docs/ folder is missing or unreadable, just skip registration - return; + const st = statSync(fullPath); + if (!st.isFile()) continue; + } catch { + continue; } - // 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); - if (!st.isFile()) continue; - } catch { - continue; - } - - // Prepare file resource info - const uri = `demo://resource/static/document/${encodeURIComponent(name)}`; - const mimeType = getMimeType(name); - const description = `Static document file exposed from /docs: ${name}`; + // Prepare file resource info + const uri = `demo://resource/static/document/${encodeURIComponent(name)}`; + const mimeType = getMimeType(name); + const description = `Static document file exposed from /docs: ${name}`; - // Register file resource - server.registerResource(name, uri, { mimeType, description }, async uri => { - const text = readFileSafe(fullPath); - return { - contents: [ - { - uri: uri.toString(), - mimeType, - text - } - ] - }; - }); - } + // Register file resource + server.registerResource( + name, + uri, + { mimeType, description }, + async (uri) => { + const text = readFileSafe(fullPath); + return { + contents: [ + { + uri: uri.toString(), + mimeType, + text, + }, + ], + }; + } + ); + } }; /** @@ -63,11 +68,12 @@ export const registerFileResources = (server: McpServer) => { * @param fileName */ function getMimeType(fileName: string): string { - const lower = fileName.toLowerCase(); - if (lower.endsWith('.md') || lower.endsWith('.markdown')) return 'text/markdown'; - if (lower.endsWith('.txt')) return 'text/plain'; - if (lower.endsWith('.json')) return 'application/json'; - return 'text/plain'; + const lower = fileName.toLowerCase(); + if (lower.endsWith(".md") || lower.endsWith(".markdown")) + return "text/markdown"; + if (lower.endsWith(".txt")) return "text/plain"; + if (lower.endsWith(".json")) return "application/json"; + return "text/plain"; } /** @@ -75,9 +81,9 @@ function getMimeType(fileName: string): string { * @param path */ function readFileSafe(path: string): string { - try { - return readFileSync(path, 'utf-8'); - } catch (e) { - return `Error reading file: ${path}. ${e}`; - } + try { + return readFileSync(path, "utf-8"); + } catch (e) { + return `Error reading file: ${path}. ${e}`; + } } diff --git a/src/everything/resources/index.ts b/src/everything/resources/index.ts index 96d27106bd..b7ec741376 100644 --- a/src/everything/resources/index.ts +++ b/src/everything/resources/index.ts @@ -1,17 +1,17 @@ -import { McpServer } from '@modelcontextprotocol/server'; -import { registerResourceTemplates } from './templates.js'; -import { registerFileResources } from './files.js'; -import { fileURLToPath } from 'url'; -import { dirname, join } from 'path'; -import { readFileSync } from 'fs'; +import { McpServer } from "@modelcontextprotocol/server"; +import { registerResourceTemplates } from "./templates.js"; +import { registerFileResources } from "./files.js"; +import { fileURLToPath } from "url"; +import { dirname, join } from "path"; +import { readFileSync } from "fs"; /** * Register the resources with the MCP server. * @param server */ export const registerResources = (server: McpServer) => { - registerResourceTemplates(server); - registerFileResources(server); + registerResourceTemplates(server); + registerFileResources(server); }; /** @@ -22,15 +22,15 @@ export const registerResources = (server: McpServer) => { * @return {string} The content of the server instructions file, or an error message if reading fails. */ export function readInstructions(): string { - const __filename = fileURLToPath(import.meta.url); - const __dirname = dirname(__filename); - const filePath = join(__dirname, '..', 'docs', 'instructions.md'); - let instructions; + const __filename = fileURLToPath(import.meta.url); + const __dirname = dirname(__filename); + const filePath = join(__dirname, "..", "docs", "instructions.md"); + let instructions; - try { - instructions = readFileSync(filePath, 'utf-8'); - } catch (e) { - instructions = 'Server instructions not loaded: ' + e; - } - return instructions; + try { + instructions = readFileSync(filePath, "utf-8"); + } catch (e) { + instructions = "Server instructions not loaded: " + e; + } + return instructions; } diff --git a/src/everything/resources/session.ts b/src/everything/resources/session.ts index 6944e7205d..9e8c33dac5 100644 --- a/src/everything/resources/session.ts +++ b/src/everything/resources/session.ts @@ -1,4 +1,9 @@ -import { McpServer, RegisteredResource, Resource, ResourceLink } from '@modelcontextprotocol/server'; +import { + McpServer, + RegisteredResource, + Resource, + ResourceLink, +} from "@modelcontextprotocol/server"; /** * Tracks registered session resources by URI to allow updating/removing on re-registration. @@ -14,7 +19,7 @@ const registeredResources = new Map(); * @returns {string} The formatted session resource URI. */ export const getSessionResourceURI = (name: string): string => { - return `demo://resource/session/${name}`; + return `demo://resource/session/${name}`; }; /** @@ -28,41 +33,52 @@ export const getSessionResourceURI = (name: string): string => { * @param payload * @returns {ResourceLink} An object representing the resource link, with associated metadata. */ -export const registerSessionResource = (server: McpServer, resource: Resource, type: 'text' | 'blob', payload: string): ResourceLink => { - // Destructure resource - const { uri, name, mimeType, description, title, annotations, icons, _meta } = resource; +export const registerSessionResource = ( + server: McpServer, + resource: Resource, + type: "text" | "blob", + payload: string +): ResourceLink => { + // Destructure resource + const { uri, name, mimeType, description, title, annotations, icons, _meta } = + resource; - // Prepare the resource content to return - // See https://modelcontextprotocol.io/specification/2025-11-25/server/resources#resource-contents - const resourceContent = - type === 'text' - ? { - uri: uri.toString(), - mimeType, - text: payload - } - : { - uri: uri.toString(), - mimeType, - blob: payload - }; + // Prepare the resource content to return + // See https://modelcontextprotocol.io/specification/2025-11-25/server/resources#resource-contents + const resourceContent = + type === "text" + ? { + uri: uri.toString(), + mimeType, + text: payload, + } + : { + uri: uri.toString(), + mimeType, + blob: payload, + }; - // Check if a resource with this URI is already registered and remove it - const existingResource = registeredResources.get(uri); - if (existingResource) { - existingResource.remove(); - registeredResources.delete(uri); - } + // Check if a resource with this URI is already registered and remove it + const existingResource = registeredResources.get(uri); + if (existingResource) { + existingResource.remove(); + registeredResources.delete(uri); + } - // Register file resource - const registeredResource = server.registerResource(name, uri, { mimeType, description, title, annotations, icons, _meta }, async () => { - return { - contents: [resourceContent] - }; - }); + // Register file resource + const registeredResource = server.registerResource( + name, + uri, + { mimeType, description, title, annotations, icons, _meta }, + async () => { + return { + contents: [resourceContent], + }; + } + ); - // Track the registered resource for potential future removal - registeredResources.set(uri, registeredResource); + // Track the registered resource for potential future removal + registeredResources.set(uri, registeredResource); - return { type: 'resource_link', ...resource }; + return { type: "resource_link", ...resource }; }; diff --git a/src/everything/resources/subscriptions.ts b/src/everything/resources/subscriptions.ts index 23e081f58a..73f027159b 100644 --- a/src/everything/resources/subscriptions.ts +++ b/src/everything/resources/subscriptions.ts @@ -1,10 +1,14 @@ -import { McpServer } from '@modelcontextprotocol/server'; +import { McpServer } from "@modelcontextprotocol/server"; // Track subscriber session id lists by URI -const subscriptions: Map> = new Map>(); +const subscriptions: Map> = new Map< + string, + Set +>(); // Interval to send notifications to subscribers -const subsUpdateIntervals: Map = new Map(); +const subsUpdateIntervals: Map = + new Map(); /** * Sets up the subscription and unsubscription handlers for the provided server. @@ -26,54 +30,66 @@ const subsUpdateIntervals: Map = * @param {McpServer} server - The server instance to which subscription handlers will be attached. */ export const setSubscriptionHandlers = (server: McpServer) => { - // Set the subscription handler - server.server.setRequestHandler('resources/subscribe', async (request, ctx) => { - // Get the URI to subscribe to - const { uri } = request.params; + // Set the subscription handler + server.server.setRequestHandler( + "resources/subscribe", + async (request, ctx) => { + // Get the URI to subscribe to + const { uri } = request.params; - // Get the session id (can be undefined for stdio) - const sessionId = ctx.sessionId as string; + // Get the session id (can be undefined for stdio) + const sessionId = ctx.sessionId as string; - // Acknowledge the subscribe request - await server.sendLoggingMessage( - { - level: 'info', - data: `Received Subscribe Resource request for URI: ${uri} ${sessionId ? `from session ${sessionId}` : ''}` - }, - sessionId - ); + // Acknowledge the subscribe request + await server.sendLoggingMessage( + { + level: "info", + data: `Received Subscribe Resource request for URI: ${uri} ${ + sessionId ? `from session ${sessionId}` : "" + }`, + }, + sessionId + ); - // Get the subscribers for this URI - const subscribers = subscriptions.has(uri) ? (subscriptions.get(uri) as Set) : new Set(); - subscribers.add(sessionId); - subscriptions.set(uri, subscribers); - return {}; - }); + // Get the subscribers for this URI + const subscribers = subscriptions.has(uri) + ? (subscriptions.get(uri) as Set) + : new Set(); + subscribers.add(sessionId); + subscriptions.set(uri, subscribers); + return {}; + } + ); - // Set the unsubscription handler - server.server.setRequestHandler('resources/unsubscribe', async (request, ctx) => { - // Get the URI to subscribe to - const { uri } = request.params; + // Set the unsubscription handler + server.server.setRequestHandler( + "resources/unsubscribe", + async (request, ctx) => { + // Get the URI to subscribe to + const { uri } = request.params; - // Get the session id (can be undefined for stdio) - const sessionId = ctx.sessionId as string; + // Get the session id (can be undefined for stdio) + const sessionId = ctx.sessionId as string; - // Acknowledge the subscribe request - await server.sendLoggingMessage( - { - level: 'info', - data: `Received Unsubscribe Resource request: ${uri} ${sessionId ? `from session ${sessionId}` : ''}` - }, - sessionId - ); + // Acknowledge the subscribe request + await server.sendLoggingMessage( + { + level: "info", + data: `Received Unsubscribe Resource request: ${uri} ${ + sessionId ? `from session ${sessionId}` : "" + }`, + }, + sessionId + ); - // Remove the subscriber - if (subscriptions.has(uri)) { - const subscribers = subscriptions.get(uri) as Set; - if (subscribers.has(sessionId)) subscribers.delete(sessionId); - } - return {}; - }); + // Remove the subscriber + if (subscriptions.has(uri)) { + const subscribers = subscriptions.get(uri) as Set; + if (subscribers.has(sessionId)) subscribers.delete(sessionId); + } + return {}; + } + ); }; /** @@ -88,21 +104,24 @@ export const setSubscriptionHandlers = (server: McpServer) => { * @param {string | undefined} sessionId - The session ID of the client to check for subscriptions. * @returns {Promise} Resolves once all applicable notifications are sent. */ -const sendSimulatedResourceUpdates = async (server: McpServer, sessionId: string | undefined): Promise => { - // Search all URIs for ones this client is subscribed to - for (const uri of subscriptions.keys()) { - const subscribers = subscriptions.get(uri) as Set; +const sendSimulatedResourceUpdates = async ( + server: McpServer, + sessionId: string | undefined +): Promise => { + // Search all URIs for ones this client is subscribed to + for (const uri of subscriptions.keys()) { + const subscribers = subscriptions.get(uri) as Set; - // If this client is subscribed, send the notification - if (subscribers.has(sessionId)) { - await server.server.notification({ - method: 'notifications/resources/updated', - params: { uri } - }); - } else { - subscribers.delete(sessionId); // subscriber has disconnected - } + // If this client is subscribed, send the notification + if (subscribers.has(sessionId)) { + await server.server.notification({ + method: "notifications/resources/updated", + params: { uri }, + }); + } else { + subscribers.delete(sessionId); // subscriber has disconnected } + } }; /** @@ -113,17 +132,20 @@ const sendSimulatedResourceUpdates = async (server: McpServer, sessionId: string * @param server * @param sessionId */ -export const beginSimulatedResourceUpdates = (server: McpServer, sessionId: string | undefined) => { - if (!subsUpdateIntervals.has(sessionId)) { - // Send once immediately - sendSimulatedResourceUpdates(server, sessionId); +export const beginSimulatedResourceUpdates = ( + server: McpServer, + sessionId: string | undefined +) => { + if (!subsUpdateIntervals.has(sessionId)) { + // Send once immediately + sendSimulatedResourceUpdates(server, sessionId); - // Set the interval to send later resource update notifications to this client - subsUpdateIntervals.set( - sessionId, - setInterval(() => sendSimulatedResourceUpdates(server, sessionId), 5000) - ); - } + // Set the interval to send later resource update notifications to this client + subsUpdateIntervals.set( + sessionId, + setInterval(() => sendSimulatedResourceUpdates(server, sessionId), 5000) + ); + } }; /** @@ -136,10 +158,10 @@ export const beginSimulatedResourceUpdates = (server: McpServer, sessionId: stri * @param {string} [sessionId] */ export const stopSimulatedResourceUpdates = (sessionId?: string) => { - // Remove active intervals - if (subsUpdateIntervals.has(sessionId)) { - const subsUpdateInterval = subsUpdateIntervals.get(sessionId); - clearInterval(subsUpdateInterval); - subsUpdateIntervals.delete(sessionId); - } + // Remove active intervals + if (subsUpdateIntervals.has(sessionId)) { + const subsUpdateInterval = subsUpdateIntervals.get(sessionId); + clearInterval(subsUpdateInterval); + subsUpdateIntervals.delete(sessionId); + } }; diff --git a/src/everything/resources/templates.ts b/src/everything/resources/templates.ts index 9a51f70ba1..434a9a3292 100644 --- a/src/everything/resources/templates.ts +++ b/src/everything/resources/templates.ts @@ -1,10 +1,18 @@ -import { z } from 'zod'; -import { CompleteResourceTemplateCallback, McpServer, ResourceTemplate, completable } from '@modelcontextprotocol/server'; +import { z } from "zod"; +import { + CompleteResourceTemplateCallback, + McpServer, + ResourceTemplate, + completable, +} from "@modelcontextprotocol/server"; // 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]; +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. @@ -16,9 +24,12 @@ export const RESOURCE_TYPES: string[] = [RESOURCE_TYPE_TEXT, RESOURCE_TYPE_BLOB] * 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)); -}); +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. @@ -36,10 +47,13 @@ export const resourceTypeCompleter = completable(z.string().describe('Type of re * 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) => { +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 @@ -50,13 +64,14 @@ export const resourceIdForPromptCompleter = completable(z.string().describe('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) => { +export const resourceIdForResourceTemplateCompleter: CompleteResourceTemplateCallback = + (value: string) => { const resourceId = Number(value); return Number.isInteger(resourceId) && resourceId > 0 ? [value] : []; -}; + }; -const uriBase: string = 'demo://resource/dynamic'; +const uriBase: string = "demo://resource/dynamic"; const textUriBase: string = `${uriBase}/text`; const blobUriBase: string = `${uriBase}/blob`; const textUriTemplate: string = `${textUriBase}/{resourceId}`; @@ -69,12 +84,12 @@ const blobUriTemplate: string = `${blobUriBase}/{resourceId}`; * @param resourceId */ export const textResource = (uri: URL, resourceId: number) => { - const timestamp = new Date().toLocaleTimeString(); - return { - uri: uri.toString(), - mimeType: 'text/plain', - text: `Resource ${resourceId}: This is a plaintext resource created at ${timestamp}` - }; + const timestamp = new Date().toLocaleTimeString(); + return { + uri: uri.toString(), + mimeType: "text/plain", + text: `Resource ${resourceId}: This is a plaintext resource created at ${timestamp}`, + }; }; /** @@ -84,13 +99,15 @@ export const textResource = (uri: URL, resourceId: number) => { * @param resourceId */ export const blobResource = (uri: URL, resourceId: number) => { - const timestamp = new Date().toLocaleTimeString(); - const resourceText = Buffer.from(`Resource ${resourceId}: This is a base64 blob created at ${timestamp}`).toString('base64'); - return { - uri: uri.toString(), - mimeType: 'text/plain', - blob: resourceText - }; + const timestamp = new Date().toLocaleTimeString(); + const resourceText = Buffer.from( + `Resource ${resourceId}: This is a base64 blob created at ${timestamp}` + ).toString("base64"); + return { + uri: uri.toString(), + mimeType: "text/plain", + blob: resourceText, + }; }; /** @@ -98,14 +115,16 @@ export const blobResource = (uri: URL, resourceId: number) => { * - Exposed for use by embedded resource prompt example * @param resourceId */ -export const textResourceUri = (resourceId: number) => new URL(`${textUriBase}/${resourceId}`); +export const textResourceUri = (resourceId: number) => + new URL(`${textUriBase}/${resourceId}`); /** * Create a dynamic blob resource URI * - Exposed for use by embedded resource prompt example * @param resourceId */ -export const blobResourceUri = (resourceId: number) => new URL(`${blobUriBase}/${resourceId}`); +export const blobResourceUri = (resourceId: number) => + new URL(`${blobUriBase}/${resourceId}`); /** * Parses the resource identifier from the provided URI and validates it @@ -118,18 +137,21 @@ export const blobResourceUri = (resourceId: number) => new URL(`${blobUriBase}/$ * @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); + 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 { - 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); - } + throw new Error(uriError); } + } }; /** @@ -147,41 +169,43 @@ const parseResourceId = (uri: URL, variables: Record) => { * @param server */ export const registerResourceTemplates = (server: McpServer) => { - // Register the text resource template - server.registerResource( - 'Dynamic Text Resource', - new ResourceTemplate(textUriTemplate, { - list: undefined, - complete: { resourceId: resourceIdForResourceTemplateCompleter } - }), - { - mimeType: 'text/plain', - description: 'Plaintext dynamic resource fabricated from the {resourceId} variable, which must be an integer.' - }, - async (uri, variables) => { - const resourceId = parseResourceId(uri, variables); - return { - contents: [textResource(uri, resourceId)] - }; - } - ); + // Register the text resource template + server.registerResource( + "Dynamic Text Resource", + new ResourceTemplate(textUriTemplate, { + list: undefined, + complete: { resourceId: resourceIdForResourceTemplateCompleter }, + }), + { + mimeType: "text/plain", + description: + "Plaintext dynamic resource fabricated from the {resourceId} variable, which must be an integer.", + }, + async (uri, variables) => { + const resourceId = parseResourceId(uri, variables); + return { + contents: [textResource(uri, resourceId)], + }; + } + ); - // Register the blob resource template - server.registerResource( - 'Dynamic Blob Resource', - new ResourceTemplate(blobUriTemplate, { - list: undefined, - complete: { resourceId: resourceIdForResourceTemplateCompleter } - }), - { - mimeType: 'application/octet-stream', - description: 'Binary (base64) dynamic resource fabricated from the {resourceId} variable, which must be an integer.' - }, - async (uri, variables) => { - const resourceId = parseResourceId(uri, variables); - return { - contents: [blobResource(uri, resourceId)] - }; - } - ); + // Register the blob resource template + server.registerResource( + "Dynamic Blob Resource", + new ResourceTemplate(blobUriTemplate, { + list: undefined, + complete: { resourceId: resourceIdForResourceTemplateCompleter }, + }), + { + mimeType: "application/octet-stream", + description: + "Binary (base64) dynamic resource fabricated from the {resourceId} variable, which must be an integer.", + }, + async (uri, variables) => { + const resourceId = parseResourceId(uri, variables); + return { + contents: [blobResource(uri, resourceId)], + }; + } + ); }; diff --git a/src/everything/server/index.ts b/src/everything/server/index.ts index 6948182777..dc28137551 100644 --- a/src/everything/server/index.ts +++ b/src/everything/server/index.ts @@ -1,15 +1,18 @@ -import { McpServer } from '@modelcontextprotocol/server'; -import { setSubscriptionHandlers, stopSimulatedResourceUpdates } from '../resources/subscriptions.js'; -import { registerConditionalTools, registerTools } from '../tools/index.js'; -import { registerResources, readInstructions } from '../resources/index.js'; -import { registerPrompts } from '../prompts/index.js'; -import { stopSimulatedLogging } from './logging.js'; -import { syncRoots } from './roots.js'; +import { McpServer } from "@modelcontextprotocol/server"; +import { + setSubscriptionHandlers, + stopSimulatedResourceUpdates, +} from "../resources/subscriptions.js"; +import { registerConditionalTools, registerTools } from "../tools/index.js"; +import { registerResources, readInstructions } from "../resources/index.js"; +import { registerPrompts } from "../prompts/index.js"; +import { stopSimulatedLogging } from "./logging.js"; +import { syncRoots } from "./roots.js"; // Server Factory response export type ServerFactoryResponse = { - server: McpServer; - cleanup: (sessionId?: string) => void; + server: McpServer; + cleanup: (sessionId?: string) => void; }; /** @@ -26,78 +29,78 @@ export type ServerFactoryResponse = { * - `cleanup` {Function}: Function to perform cleanup operations for a closing session. */ export const createServer: () => ServerFactoryResponse = () => { - // Read the server instructions - const instructions = readInstructions(); + // Read the server instructions + const instructions = readInstructions(); - let initializeTimeout: NodeJS.Timeout | null = null; + let initializeTimeout: NodeJS.Timeout | null = null; - // Create the server - const server = new McpServer( - { - name: 'mcp-servers/everything', - title: 'Everything Reference Server', - version: '2.0.0' + // Create the server + const server = new McpServer( + { + name: "mcp-servers/everything", + title: "Everything Reference Server", + version: "2.0.0", + }, + { + capabilities: { + tools: { + listChanged: true, }, - { - capabilities: { - tools: { - listChanged: true - }, - prompts: { - listChanged: true - }, - resources: { - subscribe: true, - listChanged: true - }, - logging: {}, - tasks: { - list: {}, - cancel: {}, - requests: { - tools: { - call: {} - } - } - } + prompts: { + listChanged: true, + }, + resources: { + subscribe: true, + listChanged: true, + }, + logging: {}, + tasks: { + list: {}, + cancel: {}, + requests: { + tools: { + call: {}, }, - instructions - } - ); + }, + }, + }, + instructions, + } + ); - // Register the tools - registerTools(server); + // Register the tools + registerTools(server); - // Register the resources - registerResources(server); + // Register the resources + registerResources(server); - // Register the prompts - registerPrompts(server); + // Register the prompts + registerPrompts(server); - // Set resource subscription handlers - setSubscriptionHandlers(server); + // Set resource subscription handlers + setSubscriptionHandlers(server); - // Perform post-initialization operations - server.server.oninitialized = async () => { - // Register conditional tools now that client capabilities are known. - // This finishes before the `notifications/initialized` handler finishes. - registerConditionalTools(server); + // Perform post-initialization operations + server.server.oninitialized = async () => { + // Register conditional tools now that client capabilities are known. + // This finishes before the `notifications/initialized` handler finishes. + registerConditionalTools(server); - // Sync roots if the client supports them. - // This is delayed until after the `notifications/initialized` handler finishes, - // otherwise, the request gets lost. - const sessionId = server.server.transport?.sessionId; - initializeTimeout = setTimeout(() => syncRoots(server, sessionId), 350); - }; + // Sync roots if the client supports them. + // This is delayed until after the `notifications/initialized` handler finishes, + // otherwise, the request gets lost. + const sessionId = server.server.transport?.sessionId; + initializeTimeout = setTimeout(() => syncRoots(server, sessionId), 350); + }; - // Return the ServerFactoryResponse - return { - server, - cleanup: (sessionId?: string) => { - // Stop any simulated logging or resource updates that may have been initiated. - stopSimulatedLogging(sessionId); - stopSimulatedResourceUpdates(sessionId); - if (initializeTimeout) clearTimeout(initializeTimeout); - } - } satisfies ServerFactoryResponse; + // Return the ServerFactoryResponse + return { + server, + cleanup: (sessionId?: string) => { + // Stop any simulated logging or resource updates that may have been initiated. + stopSimulatedLogging(sessionId); + stopSimulatedResourceUpdates(sessionId); + if (initializeTimeout) clearTimeout(initializeTimeout); + }, + } satisfies ServerFactoryResponse; }; diff --git a/src/everything/server/logging.ts b/src/everything/server/logging.ts index 6f54ae058e..0cce55e98f 100644 --- a/src/everything/server/logging.ts +++ b/src/everything/server/logging.ts @@ -1,7 +1,8 @@ -import { LoggingLevel, McpServer } from '@modelcontextprotocol/server'; +import { LoggingLevel, McpServer } from "@modelcontextprotocol/server"; // Map session ID to the interval for sending logging messages to the client -const logsUpdateIntervals: Map = new Map(); +const logsUpdateIntervals: Map = + new Map(); /** * Initiates a simulated logging process by sending random log messages to the client at a @@ -11,48 +12,54 @@ const logsUpdateIntervals: Map = * @param {string | undefined} sessionId - An optional identifier for the session. If provided, * the session ID will be appended to log messages. */ -export const beginSimulatedLogging = (server: McpServer, sessionId: string | undefined) => { - const maybeAppendSessionId = sessionId ? ` - SessionId ${sessionId}` : ''; - const messages: { level: LoggingLevel; data: string }[] = [ - { level: 'debug', data: `Debug-level message${maybeAppendSessionId}` }, - { level: 'info', data: `Info-level message${maybeAppendSessionId}` }, - { level: 'notice', data: `Notice-level message${maybeAppendSessionId}` }, - { - level: 'warning', - data: `Warning-level message${maybeAppendSessionId}` - }, - { level: 'error', data: `Error-level message${maybeAppendSessionId}` }, - { - level: 'critical', - data: `Critical-level message${maybeAppendSessionId}` - }, - { level: 'alert', data: `Alert level-message${maybeAppendSessionId}` }, - { - level: 'emergency', - data: `Emergency-level message${maybeAppendSessionId}` - } - ]; +export const beginSimulatedLogging = ( + server: McpServer, + sessionId: string | undefined +) => { + const maybeAppendSessionId = sessionId ? ` - SessionId ${sessionId}` : ""; + const messages: { level: LoggingLevel; data: string }[] = [ + { level: "debug", data: `Debug-level message${maybeAppendSessionId}` }, + { level: "info", data: `Info-level message${maybeAppendSessionId}` }, + { level: "notice", data: `Notice-level message${maybeAppendSessionId}` }, + { + level: "warning", + data: `Warning-level message${maybeAppendSessionId}`, + }, + { level: "error", data: `Error-level message${maybeAppendSessionId}` }, + { + level: "critical", + data: `Critical-level message${maybeAppendSessionId}`, + }, + { level: "alert", data: `Alert level-message${maybeAppendSessionId}` }, + { + level: "emergency", + data: `Emergency-level message${maybeAppendSessionId}`, + }, + ]; - /** - * Send a simulated logging message to the client - */ - const sendSimulatedLoggingMessage = async (sessionId: string | undefined) => { - // By using the `sendLoggingMessage` function to send the message, we - // ensure that the client's chosen logging level will be respected - await server.sendLoggingMessage(messages[Math.floor(Math.random() * messages.length)], sessionId); - }; + /** + * Send a simulated logging message to the client + */ + const sendSimulatedLoggingMessage = async (sessionId: string | undefined) => { + // By using the `sendLoggingMessage` function to send the message, we + // ensure that the client's chosen logging level will be respected + await server.sendLoggingMessage( + messages[Math.floor(Math.random() * messages.length)], + sessionId + ); + }; - // Set the interval to send later logging messages to this client - if (!logsUpdateIntervals.has(sessionId)) { - // Send once immediately - sendSimulatedLoggingMessage(sessionId); + // Set the interval to send later logging messages to this client + if (!logsUpdateIntervals.has(sessionId)) { + // Send once immediately + sendSimulatedLoggingMessage(sessionId); - // Send a randomly-leveled log message every 5 seconds - logsUpdateIntervals.set( - sessionId, - setInterval(() => sendSimulatedLoggingMessage(sessionId), 5000) - ); - } + // Send a randomly-leveled log message every 5 seconds + logsUpdateIntervals.set( + sessionId, + setInterval(() => sendSimulatedLoggingMessage(sessionId), 5000) + ); + } }; /** @@ -65,10 +72,10 @@ export const beginSimulatedLogging = (server: McpServer, sessionId: string | und * @param {string} [sessionId] - The optional unique identifier of the session. */ export const stopSimulatedLogging = (sessionId?: string) => { - // Remove active intervals - if (logsUpdateIntervals.has(sessionId)) { - const logsUpdateInterval = logsUpdateIntervals.get(sessionId); - clearInterval(logsUpdateInterval); - logsUpdateIntervals.delete(sessionId); - } + // Remove active intervals + if (logsUpdateIntervals.has(sessionId)) { + const logsUpdateInterval = logsUpdateIntervals.get(sessionId); + clearInterval(logsUpdateInterval); + logsUpdateIntervals.delete(sessionId); + } }; diff --git a/src/everything/server/roots.ts b/src/everything/server/roots.ts index 00d6d3701f..d0a4146ebb 100644 --- a/src/everything/server/roots.ts +++ b/src/everything/server/roots.ts @@ -1,7 +1,10 @@ -import { McpServer, Root } from '@modelcontextprotocol/server'; +import { McpServer, Root } from "@modelcontextprotocol/server"; // Track roots by session id -export const roots: Map = new Map(); +export const roots: Map = new Map< + string | undefined, + Root[] +>(); /** * Get the latest the client roots list for the session. @@ -22,57 +25,62 @@ export const roots: Map = new Map { - const clientCapabilities = server.server.getClientCapabilities() || {}; - const clientSupportsRoots: boolean = clientCapabilities?.roots !== undefined; + const clientCapabilities = server.server.getClientCapabilities() || {}; + const clientSupportsRoots: boolean = clientCapabilities?.roots !== undefined; - // Fetch the roots list for this client - if (clientSupportsRoots) { - // Function to request the updated roots list from the client - const requestRoots = async () => { - try { - // Request the updated roots list from the client - const response = await server.server.listRoots(); - if (response && 'roots' in response) { - // Store the roots list for this client - roots.set(sessionId, response.roots); + // Fetch the roots list for this client + if (clientSupportsRoots) { + // Function to request the updated roots list from the client + const requestRoots = async () => { + try { + // Request the updated roots list from the client + const response = await server.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 - ); - } else { - await server.sendLoggingMessage( - { - level: 'info', - logger: 'everything-server', - data: 'Client returned no roots set' - }, - sessionId - ); - } - } catch (error) { - console.error( - `Failed to request roots from client ${sessionId}: ${error instanceof Error ? error.message : String(error)}` - ); - } - }; - - // If the roots have not been synced for this client, - // set notification handler and request initial roots - if (!roots.has(sessionId)) { - // Set the list changed notification handler - server.server.setNotificationHandler('notifications/roots/list_changed', requestRoots); - - // Request the initial roots list immediately - await requestRoots(); + // 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 + ); + } else { + await server.sendLoggingMessage( + { + level: "info", + logger: "everything-server", + data: "Client returned no roots set", + }, + sessionId + ); } + } catch (error) { + console.error( + `Failed to request roots from client ${sessionId}: ${ + error instanceof Error ? error.message : String(error) + }` + ); + } + }; - // Return the roots list for this client - return roots.get(sessionId); + // If the roots have not been synced for this client, + // set notification handler and request initial roots + if (!roots.has(sessionId)) { + // Set the list changed notification handler + server.server.setNotificationHandler( + "notifications/roots/list_changed", + requestRoots + ); + + // Request the initial roots list immediately + await requestRoots(); } + + // Return the roots list for this client + return roots.get(sessionId); + } }; diff --git a/src/everything/tools/echo.ts b/src/everything/tools/echo.ts index db5a4db836..8ae7943ad5 100644 --- a/src/everything/tools/echo.ts +++ b/src/everything/tools/echo.ts @@ -1,23 +1,23 @@ -import { McpServer, CallToolResult } from '@modelcontextprotocol/server'; -import { z } from 'zod'; +import { McpServer, CallToolResult } from "@modelcontextprotocol/server"; +import { z } from "zod"; // Tool input schema export const EchoSchema = z.object({ - message: z.string().describe('Message to echo') + message: z.string().describe("Message to echo"), }); // Tool configuration -const name = 'echo'; +const name = "echo"; const config = { - title: 'Echo Tool', - description: 'Echoes back the input string', - inputSchema: EchoSchema, - annotations: { - readOnlyHint: true, - destructiveHint: false, - idempotentHint: true, - openWorldHint: false - } + title: "Echo Tool", + description: "Echoes back the input string", + inputSchema: EchoSchema, + annotations: { + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: false, + }, }; /** @@ -30,10 +30,10 @@ const config = { * @returns {void} */ export const registerEchoTool = (server: McpServer) => { - server.registerTool(name, config, async (args): Promise => { - const validatedArgs = EchoSchema.parse(args); - return { - content: [{ type: 'text', text: `Echo: ${validatedArgs.message}` }] - }; - }); + server.registerTool(name, config, async (args): Promise => { + const validatedArgs = EchoSchema.parse(args); + return { + content: [{ type: "text", text: `Echo: ${validatedArgs.message}` }], + }; + }); }; diff --git a/src/everything/tools/get-annotated-message.ts b/src/everything/tools/get-annotated-message.ts index 743f8c97ad..d1d26dbef0 100644 --- a/src/everything/tools/get-annotated-message.ts +++ b/src/everything/tools/get-annotated-message.ts @@ -1,25 +1,31 @@ -import { McpServer, CallToolResult } from '@modelcontextprotocol/server'; -import { z } from 'zod'; -import { MCP_TINY_IMAGE } from './get-tiny-image.js'; +import { McpServer, CallToolResult } from "@modelcontextprotocol/server"; +import { z } from "zod"; +import { MCP_TINY_IMAGE } from "./get-tiny-image.js"; // Tool input schema const GetAnnotatedMessageSchema = z.object({ - messageType: z.enum(['error', 'success', 'debug']).describe('Type of message to demonstrate different annotation patterns'), - includeImage: z.boolean().default(false).describe('Whether to include an example image') + messageType: z + .enum(["error", "success", "debug"]) + .describe("Type of message to demonstrate different annotation patterns"), + includeImage: z + .boolean() + .default(false) + .describe("Whether to include an example image"), }); // Tool configuration -const name = 'get-annotated-message'; +const name = "get-annotated-message"; const config = { - title: 'Get Annotated Message Tool', - description: 'Demonstrates how annotations can be used to provide metadata about content.', - inputSchema: GetAnnotatedMessageSchema, - annotations: { - readOnlyHint: true, // This tool only returns data, no side effects - destructiveHint: false, // Does not delete or modify anything - idempotentHint: true, // Same input always produces same output - openWorldHint: false // Does not interact with external systems - } + title: "Get Annotated Message Tool", + description: + "Demonstrates how annotations can be used to provide metadata about content.", + inputSchema: GetAnnotatedMessageSchema, + annotations: { + readOnlyHint: true, // This tool only returns data, no side effects + destructiveHint: false, // Does not delete or modify anything + idempotentHint: true, // Same input always produces same output + openWorldHint: false, // Does not interact with external systems + }, }; /** @@ -35,54 +41,54 @@ const config = { * @param {McpServer} server - The McpServer instance where the tool will be registered. */ export const registerGetAnnotatedMessageTool = (server: McpServer) => { - server.registerTool(name, config, async (args): Promise => { - const { messageType, includeImage } = GetAnnotatedMessageSchema.parse(args); + server.registerTool(name, config, async (args): Promise => { + const { messageType, includeImage } = GetAnnotatedMessageSchema.parse(args); - const content: CallToolResult['content'] = []; + const content: CallToolResult["content"] = []; - // Main message with different priorities/audiences based on type - if (messageType === 'error') { - content.push({ - type: 'text', - text: 'Error: Operation failed', - annotations: { - priority: 1.0, // Errors are highest priority - audience: ['user', 'assistant'] // Both need to know about errors - } - }); - } else if (messageType === 'success') { - content.push({ - type: 'text', - text: 'Operation completed successfully', - annotations: { - priority: 0.7, // Success messages are important but not critical - audience: ['user'] // Success mainly for user consumption - } - }); - } else if (messageType === 'debug') { - content.push({ - type: 'text', - text: 'Debug: Cache hit ratio 0.95, latency 150ms', - annotations: { - priority: 0.3, // Debug info is low priority - audience: ['assistant'] // Technical details for assistant - } - }); - } + // Main message with different priorities/audiences based on type + if (messageType === "error") { + content.push({ + type: "text", + text: "Error: Operation failed", + annotations: { + priority: 1.0, // Errors are highest priority + audience: ["user", "assistant"], // Both need to know about errors + }, + }); + } else if (messageType === "success") { + content.push({ + type: "text", + text: "Operation completed successfully", + annotations: { + priority: 0.7, // Success messages are important but not critical + audience: ["user"], // Success mainly for user consumption + }, + }); + } else if (messageType === "debug") { + content.push({ + type: "text", + text: "Debug: Cache hit ratio 0.95, latency 150ms", + annotations: { + priority: 0.3, // Debug info is low priority + audience: ["assistant"], // Technical details for assistant + }, + }); + } - // Optional image with its own annotations - if (includeImage) { - content.push({ - type: 'image', - data: MCP_TINY_IMAGE, - mimeType: 'image/png', - annotations: { - priority: 0.5, - audience: ['user'] // Images primarily for user visualization - } - }); - } + // Optional image with its own annotations + if (includeImage) { + content.push({ + type: "image", + data: MCP_TINY_IMAGE, + mimeType: "image/png", + annotations: { + priority: 0.5, + audience: ["user"], // Images primarily for user visualization + }, + }); + } - return { content }; - }); + return { content }; + }); }; diff --git a/src/everything/tools/get-env.ts b/src/everything/tools/get-env.ts index 7e0a16109b..bbe447798b 100644 --- a/src/everything/tools/get-env.ts +++ b/src/everything/tools/get-env.ts @@ -1,17 +1,18 @@ -import { McpServer, CallToolResult } from '@modelcontextprotocol/server'; +import { McpServer, CallToolResult } from "@modelcontextprotocol/server"; // Tool configuration -const name = 'get-env'; +const name = "get-env"; const config = { - title: 'Print Environment Tool', - description: 'Returns all environment variables, helpful for debugging MCP server configuration', - inputSchema: {}, - annotations: { - readOnlyHint: true, - destructiveHint: false, - idempotentHint: true, - openWorldHint: false - } + title: "Print Environment Tool", + description: + "Returns all environment variables, helpful for debugging MCP server configuration", + inputSchema: {}, + annotations: { + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: false, + }, }; /** @@ -24,14 +25,14 @@ const config = { * @returns {void} */ export const registerGetEnvTool = (server: McpServer) => { - server.registerTool(name, config, async (args): Promise => { - return { - content: [ - { - type: 'text', - text: JSON.stringify(process.env, null, 2) - } - ] - }; - }); + server.registerTool(name, config, async (args): Promise => { + return { + content: [ + { + type: "text", + text: JSON.stringify(process.env, null, 2), + }, + ], + }; + }); }; diff --git a/src/everything/tools/get-resource-links.ts b/src/everything/tools/get-resource-links.ts index 06ebf8f4cc..e35bf83631 100644 --- a/src/everything/tools/get-resource-links.ts +++ b/src/everything/tools/get-resource-links.ts @@ -1,24 +1,35 @@ -import { z } from 'zod'; -import { McpServer, CallToolResult } from '@modelcontextprotocol/server'; -import { textResource, textResourceUri, blobResourceUri, blobResource } from '../resources/templates.js'; +import { z } from "zod"; +import { McpServer, CallToolResult } from "@modelcontextprotocol/server"; +import { + textResource, + textResourceUri, + blobResourceUri, + blobResource, +} from "../resources/templates.js"; // Tool input schema const GetResourceLinksSchema = z.object({ - count: z.number().min(1).max(10).default(3).describe('Number of resource links to return (1-10)') + count: z + .number() + .min(1) + .max(10) + .default(3) + .describe("Number of resource links to return (1-10)"), }); // Tool configuration -const name = 'get-resource-links'; +const name = "get-resource-links"; const config = { - title: 'Get Resource Links Tool', - description: 'Returns up to ten resource links that reference different types of resources', - inputSchema: GetResourceLinksSchema, - annotations: { - readOnlyHint: true, - destructiveHint: false, - idempotentHint: true, - openWorldHint: false - } + title: "Get Resource Links Tool", + description: + "Returns up to ten resource links that reference different types of resources", + inputSchema: GetResourceLinksSchema, + annotations: { + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: false, + }, }; /** @@ -33,36 +44,42 @@ const config = { * @param {McpServer} server - The McpServer instance where the tool will be registered. */ export const registerGetResourceLinksTool = (server: McpServer) => { - server.registerTool(name, config, async (args): Promise => { - const { count } = GetResourceLinksSchema.parse(args); + server.registerTool(name, config, async (args): Promise => { + const { count } = GetResourceLinksSchema.parse(args); - // Add intro text content block - const content: CallToolResult['content'] = []; - content.push({ - type: 'text', - text: `Here are ${count} resource links to resources available in this server:` - }); + // Add intro text content block + const content: CallToolResult["content"] = []; + content.push({ + type: "text", + text: `Here are ${count} resource links to resources available in this server:`, + }); - // Create resource link content blocks - for (let resourceId = 1; resourceId <= count; resourceId++) { - // Get resource uri for text or blob resource based on odd/even resourceId - const isOdd = resourceId % 2 === 0; - const uri = isOdd ? textResourceUri(resourceId) : blobResourceUri(resourceId); + // Create resource link content blocks + for (let resourceId = 1; resourceId <= count; resourceId++) { + // Get resource uri for text or blob resource based on odd/even resourceId + const isOdd = resourceId % 2 === 0; + const uri = isOdd + ? textResourceUri(resourceId) + : blobResourceUri(resourceId); - // Get resource based on the resource type - const resource = isOdd ? textResource(uri, resourceId) : blobResource(uri, resourceId); + // Get resource based on the resource type + const resource = isOdd + ? textResource(uri, resourceId) + : blobResource(uri, resourceId); - content.push({ - type: 'resource_link', - uri: resource.uri, - name: `${isOdd ? 'Text' : 'Blob'} Resource ${resourceId}`, - description: `Resource ${resourceId}: ${ - resource.mimeType === 'text/plain' ? 'plaintext resource' : 'binary blob resource' - }`, - mimeType: resource.mimeType - }); - } + content.push({ + type: "resource_link", + uri: resource.uri, + name: `${isOdd ? "Text" : "Blob"} Resource ${resourceId}`, + description: `Resource ${resourceId}: ${ + resource.mimeType === "text/plain" + ? "plaintext resource" + : "binary blob resource" + }`, + mimeType: resource.mimeType, + }); + } - return { content }; - }); + return { content }; + }); }; diff --git a/src/everything/tools/get-resource-reference.ts b/src/everything/tools/get-resource-reference.ts index a872eb91a8..b11020b943 100644 --- a/src/everything/tools/get-resource-reference.ts +++ b/src/everything/tools/get-resource-reference.ts @@ -1,33 +1,38 @@ -import { z } from 'zod'; -import { McpServer, CallToolResult } from '@modelcontextprotocol/server'; +import { z } from "zod"; +import { McpServer, CallToolResult } from "@modelcontextprotocol/server"; import { - textResource, - textResourceUri, - blobResourceUri, - blobResource, - RESOURCE_TYPE_BLOB, - RESOURCE_TYPE_TEXT, - RESOURCE_TYPES -} from '../resources/templates.js'; + 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') + 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 name = "get-resource-reference"; const config = { - title: 'Get Resource Reference Tool', - description: 'Returns a resource reference that can be used by MCP clients', - inputSchema: GetResourceReferenceSchema, - annotations: { - readOnlyHint: true, - destructiveHint: false, - idempotentHint: true, - openWorldHint: false - } + title: "Get Resource Reference Tool", + description: "Returns a resource reference that can be used by MCP clients", + inputSchema: GetResourceReferenceSchema, + annotations: { + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: false, + }, }; /** @@ -47,38 +52,52 @@ const config = { * @param {McpServer} server - The McpServer instance where the tool will be 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}.`); - } + 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.`); - } + // 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); + // 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}` - } - ] - }; - }); + 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/get-roots-list.ts b/src/everything/tools/get-roots-list.ts index b3daaf5c44..bc309e33c4 100644 --- a/src/everything/tools/get-roots-list.ts +++ b/src/everything/tools/get-roots-list.ts @@ -1,19 +1,19 @@ -import { McpServer, CallToolResult } from '@modelcontextprotocol/server'; -import { syncRoots } from '../server/roots.js'; +import { McpServer, CallToolResult } from "@modelcontextprotocol/server"; +import { syncRoots } from "../server/roots.js"; // Tool configuration -const name = 'get-roots-list'; +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: {}, - annotations: { - readOnlyHint: true, - destructiveHint: false, - idempotentHint: true, - openWorldHint: false - } + 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: {}, + annotations: { + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: false, + }, }; /** @@ -34,53 +34,64 @@ const config = { * @param {McpServer} server - The McpServer instance where the tool will be registered. */ export const registerGetRootsListTool = (server: McpServer) => { - // Does client support roots? - const clientCapabilities = server.server.getClientCapabilities() || {}; - const clientSupportsRoots: boolean = clientCapabilities.roots !== undefined; + // Does client support roots? + const clientCapabilities = server.server.getClientCapabilities() || {}; + const clientSupportsRoots: boolean = clientCapabilities.roots !== undefined; - // If so, register tool - if (clientSupportsRoots) { - server.registerTool(name, config, async (args, ctx): Promise => { - // Get the current rootsFetch the current roots list from the client if need be - const currentRoots = await syncRoots(server, ctx.sessionId); + // If so, register tool + if (clientSupportsRoots) { + server.registerTool( + name, + config, + async (args, ctx): Promise => { + // Get the current rootsFetch the current roots list from the client if need be + const currentRoots = await syncRoots(server, ctx.sessionId); - // Respond if client supports roots but doesn't have any configured - if (clientSupportsRoots && (!currentRoots || 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' - } - ] - }; - } + // Respond if client supports roots but doesn't have any configured + if ( + clientSupportsRoots && + (!currentRoots || 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", + }, + ], + }; + } - // Create formatted response if there is a list of roots - const rootsList = currentRoots - ? currentRoots - .map((root, index) => { - return `${index + 1}. ${root.name || 'Unnamed Root'}\n URI: ${root.uri}`; - }) - .join('\n\n') - : 'No roots found'; + // Create formatted response if there is a list of roots + const rootsList = currentRoots + ? currentRoots + .map((root, index) => { + return `${index + 1}. ${root.name || "Unnamed Root"}\n URI: ${ + root.uri + }`; + }) + .join("\n\n") + : "No roots found"; - 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.' - } - ] - }; - }); - } + 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.", + }, + ], + }; + } + ); + } }; diff --git a/src/everything/tools/get-structured-content.ts b/src/everything/tools/get-structured-content.ts index b9bcc4cca5..11f323e14b 100644 --- a/src/everything/tools/get-structured-content.ts +++ b/src/everything/tools/get-structured-content.ts @@ -1,31 +1,38 @@ -import { z } from 'zod'; -import { McpServer, CallToolResult, ContentBlock } from '@modelcontextprotocol/server'; +import { z } from "zod"; +import { + McpServer, + CallToolResult, + ContentBlock, +} from "@modelcontextprotocol/server"; // Tool input schema const GetStructuredContentInputSchema = { - location: z.enum(['New York', 'Chicago', 'Los Angeles']).describe('Choose city') + location: z + .enum(["New York", "Chicago", "Los Angeles"]) + .describe("Choose city"), }; // Tool output schema const GetStructuredContentOutputSchema = z.object({ - temperature: z.number().describe('Temperature in celsius'), - conditions: z.string().describe('Weather conditions description'), - humidity: z.number().describe('Humidity percentage') + temperature: z.number().describe("Temperature in celsius"), + conditions: z.string().describe("Weather conditions description"), + humidity: z.number().describe("Humidity percentage"), }); // Tool configuration -const name = 'get-structured-content'; +const name = "get-structured-content"; const config = { - title: 'Get Structured Content Tool', - description: 'Returns structured content along with an output schema for client data validation', - inputSchema: GetStructuredContentInputSchema, - outputSchema: GetStructuredContentOutputSchema, - annotations: { - readOnlyHint: true, - destructiveHint: false, - idempotentHint: true, - openWorldHint: false - } + title: "Get Structured Content Tool", + description: + "Returns structured content along with an output schema for client data validation", + inputSchema: GetStructuredContentInputSchema, + outputSchema: GetStructuredContentOutputSchema, + annotations: { + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: false, + }, }; /** @@ -43,43 +50,43 @@ const config = { * @param {McpServer} server - The McpServer instance where the tool will be registered. */ export const registerGetStructuredContentTool = (server: McpServer) => { - server.registerTool(name, config, async (args): Promise => { - // Get simulated weather for the chosen city - let weather; - switch (args.location) { - case 'New York': - weather = { - temperature: 33, - conditions: 'Cloudy', - humidity: 82 - }; - break; - - case 'Chicago': - weather = { - temperature: 36, - conditions: 'Light rain / drizzle', - humidity: 82 - }; - break; - - case 'Los Angeles': - weather = { - temperature: 73, - conditions: 'Sunny / Clear', - humidity: 48 - }; - break; - } + server.registerTool(name, config, async (args): Promise => { + // Get simulated weather for the chosen city + let weather; + switch (args.location) { + case "New York": + weather = { + temperature: 33, + conditions: "Cloudy", + humidity: 82, + }; + break; - const backwardCompatibleContentBlock: ContentBlock = { - type: 'text', - text: JSON.stringify(weather) + case "Chicago": + weather = { + temperature: 36, + conditions: "Light rain / drizzle", + humidity: 82, }; + break; - return { - content: [backwardCompatibleContentBlock], - structuredContent: weather + case "Los Angeles": + weather = { + temperature: 73, + conditions: "Sunny / Clear", + humidity: 48, }; - }); + break; + } + + const backwardCompatibleContentBlock: ContentBlock = { + type: "text", + text: JSON.stringify(weather), + }; + + return { + content: [backwardCompatibleContentBlock], + structuredContent: weather, + }; + }); }; diff --git a/src/everything/tools/get-sum.ts b/src/everything/tools/get-sum.ts index dcee73e483..d8bd04234e 100644 --- a/src/everything/tools/get-sum.ts +++ b/src/everything/tools/get-sum.ts @@ -1,24 +1,24 @@ -import { z } from 'zod'; -import { McpServer, CallToolResult } from '@modelcontextprotocol/server'; +import { z } from "zod"; +import { McpServer, CallToolResult } from "@modelcontextprotocol/server"; // Tool input schema const GetSumSchema = z.object({ - a: z.number().describe('First number'), - b: z.number().describe('Second number') + a: z.number().describe("First number"), + b: z.number().describe("Second number"), }); // Tool configuration -const name = 'get-sum'; +const name = "get-sum"; const config = { - title: 'Get Sum Tool', - description: 'Returns the sum of two numbers', - inputSchema: GetSumSchema, - annotations: { - readOnlyHint: true, - destructiveHint: false, - idempotentHint: true, - openWorldHint: false - } + title: "Get Sum Tool", + description: "Returns the sum of two numbers", + inputSchema: GetSumSchema, + annotations: { + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: false, + }, }; /** @@ -35,16 +35,16 @@ const config = { * @param {McpServer} server - The McpServer instance where the tool will be registered. */ export const registerGetSumTool = (server: McpServer) => { - server.registerTool(name, config, async (args): Promise => { - const validatedArgs = GetSumSchema.parse(args); - const sum = validatedArgs.a + validatedArgs.b; - return { - content: [ - { - type: 'text', - text: `The sum of ${validatedArgs.a} and ${validatedArgs.b} is ${sum}.` - } - ] - }; - }); + server.registerTool(name, config, async (args): Promise => { + const validatedArgs = GetSumSchema.parse(args); + const sum = validatedArgs.a + validatedArgs.b; + return { + content: [ + { + type: "text", + text: `The sum of ${validatedArgs.a} and ${validatedArgs.b} is ${sum}.`, + }, + ], + }; + }); }; diff --git a/src/everything/tools/get-tiny-image.ts b/src/everything/tools/get-tiny-image.ts index bf34107e87..e6355ca488 100644 --- a/src/everything/tools/get-tiny-image.ts +++ b/src/everything/tools/get-tiny-image.ts @@ -1,21 +1,21 @@ -import { McpServer, CallToolResult } from '@modelcontextprotocol/server'; +import { McpServer, CallToolResult } from "@modelcontextprotocol/server"; // A tiny encoded MCP logo image export const MCP_TINY_IMAGE = - 'iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAKsGlDQ1BJQ0MgUHJvZmlsZQAASImVlwdUU+kSgOfe9JDQEiIgJfQmSCeAlBBaAAXpYCMkAUKJMRBU7MriClZURLCs6KqIgo0idizYFsWC3QVZBNR1sWDDlXeBQ9jdd9575805c+a7c+efmf+e/z9nLgCdKZDJMlF1gCxpjjwyyI8dn5DIJvUABRiY0kBdIMyWcSMiwgCTUft3+dgGyJC9YzuU69/f/1fREImzhQBIBMbJomxhFsbHMe0TyuQ5ALg9mN9kbo5siK9gzJRjDWL8ZIhTR7hviJOHGY8fjomO5GGsDUCmCQTyVACaKeZn5wpTsTw0f4ztpSKJFGPsGbyzsmaLMMbqgiUWI8N4KD8n+S95Uv+WM1mZUyBIVfLIXoaF7C/JlmUK5v+fn+N/S1amYrSGOaa0NHlwJGaxvpAHGbNDlSxNnhI+yhLRcPwwpymCY0ZZmM1LHGWRwD9UuTZzStgop0gC+co8OfzoURZnB0SNsnx2pLJWipzHHWWBfKyuIiNG6U8T85X589Ki40Y5VxI7ZZSzM6JCx2J4Sr9cEansXywN8hurG6jce1b2X/Yr4SvX5qRFByv3LhjrXyzljuXMjlf2JhL7B4zFxCjjZTl+ylqyzAhlvDgzSOnPzo1Srs3BDuTY2gjlN0wXhESMMoRBELAhBjIhB+QggECQgBTEOeJ5Q2cUeLNl8+WS1LQcNhe7ZWI2Xyq0m8B2tHd0Bhi6syNH4j1r+C4irGtjvhWVAF4nBgcHT475Qm4BHEkCoNaO+SxnAKh3A1w5JVTIc0d8Q9cJCEAFNWCCDhiACViCLTiCK3iCLwRACIRDNCTATBBCGmRhnc+FhbAMCqAI1sNmKIOdsBv2wyE4CvVwCs7DZbgOt+AePIZ26IJX0AcfYQBBEBJCRxiIDmKImCE2iCPCQbyRACQMiUQSkCQkFZEiCmQhsgIpQoqRMmQXUokcQU4g55GrSCvyEOlAepF3yFcUh9JQJqqPmqMTUQ7KRUPRaHQGmorOQfPQfHQtWopWoAfROvQ8eh29h7ajr9B+HOBUcCycEc4Wx8HxcOG4RFwKTo5bjCvEleAqcNW4Rlwz7g6uHfca9wVPxDPwbLwt3hMfjI/BC/Fz8Ivxq/Fl+P34OvxF/B18B74P/51AJ+gRbAgeBD4hnpBKmEsoIJQQ9hJqCZcI9whdhI9EIpFFtCC6EYOJCcR04gLiauJ2Yg3xHLGV2EnsJ5FIOiQbkhcpnCQg5ZAKSFtJB0lnSbdJXaTPZBWyIdmRHEhOJEvJy8kl5APkM+Tb5G7yAEWdYkbxoIRTRJT5lHWUPZRGyk1KF2WAqkG1oHpRo6np1GXUUmo19RL1CfW9ioqKsYq7ylQVicpSlVKVwypXVDpUvtA0adY0Hm06TUFbS9tHO0d7SHtPp9PN6b70RHoOfS29kn6B/oz+WZWhaqfKVxWpLlEtV61Tva36Ro2iZqbGVZuplqdWonZM7abaa3WKurk6T12gvli9XP2E+n31fg2GhoNGuEaWxmqNAxpXNXo0SZrmmgGaIs18zd2aFzQ7GTiGCYPHEDJWMPYwLjG6mESmBZPPTGcWMQ8xW5h9WppazlqxWvO0yrVOa7WzcCxzFp+VyVrHOspqY30dpz+OO048btW46nG3x33SHq/tqy3WLtSu0b6n/VWHrROgk6GzQade56kuXtdad6ruXN0dupd0X49njvccLxxfOP7o+Ed6qJ61XqTeAr3dejf0+vUN9IP0Zfpb9S/ovzZgGfgapBtsMjhj0GvIMPQ2lBhuMjxr+JKtxeayM9ml7IvsPiM9o2AjhdEuoxajAWML4xjj5cY1xk9NqCYckxSTTSZNJn2mhqaTTReaVpk+MqOYcczSzLaYNZt9MrcwjzNfaV5v3mOhbcG3yLOosnhiSbf0sZxjWWF514poxbHKsNpudcsatXaxTrMut75pg9q42khsttu0TiBMcJ8gnVAx4b4tzZZrm2tbZdthx7ILs1tuV2/3ZqLpxMSJGyY2T/xu72Kfab/H/rGDpkOIw3KHRod3jtaOQsdyx7tOdKdApyVODU5vnW2cxc47nB+4MFwmu6x0aXL509XNVe5a7drrZuqW5LbN7T6HyYngrOZccSe4+7kvcT/l/sXD1SPH46jHH562nhmeBzx7JllMEk/aM6nTy9hL4LXLq92b7Z3k/ZN3u4+Rj8Cnwue5r4mvyHevbzfXipvOPch942fvJ/er9fvE8+At4p3zx/kH+Rf6twRoBsQElAU8CzQOTA2sCuwLcglaEHQumBAcGrwh+D5fny/kV/L7QtxCFoVcDKWFRoWWhT4Psw6ThzVORieHTN44+ckUsynSKfXhEM4P3xj+NMIiYk7EyanEqRFTy6e+iHSIXBjZHMWImhV1IOpjtF/0uujHMZYxipimWLXY6bGVsZ/i/OOK49rjJ8Yvir+eoJsgSWhIJCXGJu5N7J8WMG3ztK7pLtMLprfNsJgxb8bVmbozM2eenqU2SzDrWBIhKS7pQNI3QbigQtCfzE/eltwn5Am3CF+JfEWbRL1iL3GxuDvFK6U4pSfVK3Vjam+aT1pJ2msJT1ImeZsenL4z/VNGeMa+jMHMuMyaLHJWUtYJqaY0Q3pxtsHsebNbZTayAln7HI85m+f0yUPle7OR7BnZDTlMbDi6obBU/KDoyPXOLc/9PDd27rF5GvOk827Mt56/an53XmDezwvwC4QLmhYaLVy2sGMRd9Guxcji5MVNS0yW5C/pWhq0dP8y6rKMZb8st19evPzDirgVjfn6+UvzO38I+qGqQLVAXnB/pefKnT/if5T82LLKadXWVd8LRYXXiuyLSoq+rRauvrbGYU3pmsG1KWtb1rmu27GeuF66vm2Dz4b9xRrFecWdGydvrNvE3lS46cPmWZuvljiX7NxC3aLY0l4aVtqw1XTr+q3fytLK7pX7ldds09u2atun7aLtt3f47qjeqb+zaOfXnyQ/PdgVtKuuwryiZDdxd+7uF3ti9zT/zPm5cq/u3qK9f+6T7mvfH7n/YqVbZeUBvQPrqtAqRVXvwekHbx3yP9RQbVu9q4ZVU3QYDisOvzySdKTtaOjRpmOcY9XHzY5vq2XUFtYhdfPr+urT6tsbEhpaT4ScaGr0bKw9aXdy3ymjU+WntU6vO0M9k39m8Gze2f5zsnOvz6ee72ya1fT4QvyFuxenXmy5FHrpyuXAyxeauc1nr3hdOXXV4+qJa5xr9dddr9fdcLlR+4vLL7Utri11N91uNtzyv9XYOqn1zG2f2+fv+N+5fJd/9/q9Kfda22LaHtyffr/9gehBz8PMh28f5T4aeLz0CeFJ4VP1pyXP9J5V/Gr1a027a/vpDv+OG8+jnj/uFHa++i37t29d+S/oL0q6Dbsrexx7TvUG9t56Oe1l1yvZq4HXBb9r/L7tjeWb43/4/nGjL76v66387eC71e913u/74PyhqT+i/9nHrI8Dnwo/63ze/4Xzpflr3NfugbnfSN9K/7T6s/F76Pcng1mDgzKBXDA8CuAwRVNSAN7tA6AnADCwGYI6bWSmHhZk5D9gmOA/8cjcPSyuANWYGRqNeOcADmNqvhRAzRdgaCyK9gXUyUmpo/Pv8Kw+JAbYv8K0HECi2x6tebQU/iEjc/xf+v6nBWXWv9l/AV0EC6JTIblRAAAAeGVYSWZNTQAqAAAACAAFARIAAwAAAAEAAQAAARoABQAAAAEAAABKARsABQAAAAEAAABSASgAAwAAAAEAAgAAh2kABAAAAAEAAABaAAAAAAAAAJAAAAABAAAAkAAAAAEAAqACAAQAAAABAAAAFKADAAQAAAABAAAAFAAAAAAXNii1AAAACXBIWXMAABYlAAAWJQFJUiTwAAAB82lUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNi4wLjAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyI+CiAgICAgICAgIDx0aWZmOllSZXNvbHV0aW9uPjE0NDwvdGlmZjpZUmVzb2x1dGlvbj4KICAgICAgICAgPHRpZmY6T3JpZW50YXRpb24+MTwvdGlmZjpPcmllbnRhdGlvbj4KICAgICAgICAgPHRpZmY6WFJlc29sdXRpb24+MTQ0PC90aWZmOlhSZXNvbHV0aW9uPgogICAgICAgICA8dGlmZjpSZXNvbHV0aW9uVW5pdD4yPC90aWZmOlJlc29sdXRpb25Vbml0PgogICAgICA8L3JkZjpEZXNjcmlwdGlvbj4KICAgPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4KReh49gAAAjRJREFUOBGFlD2vMUEUx2clvoNCcW8hCqFAo1dKhEQpvsF9KrWEBh/ALbQ0KkInBI3SWyGPCCJEQliXgsTLefaca/bBWjvJzs6cOf/fnDkzOQJIjWm06/XKBEGgD8c6nU5VIWgBtQDPZPWtJE8O63a7LBgMMo/Hw0ql0jPjcY4RvmqXy4XMjUYDUwLtdhtmsxnYbDbI5/O0djqdFFKmsEiGZ9jP9gem0yn0ej2Yz+fg9XpfycimAD7DttstQTDKfr8Po9GIIg6Hw1Cr1RTgB+A72GAwgMPhQLBMJgNSXsFqtUI2myUo18pA6QJogefsPrLBX4QdCVatViklw+EQRFGEj88P2O12pEUGATmsXq+TaLPZ0AXgMRF2vMEqlQoJTSYTpNNpApvNZliv1/+BHDaZTAi2Wq1A3Ig0xmMej7+RcZjdbodUKkWAaDQK+GHjHPnImB88JrZIJAKFQgH2+z2BOczhcMiwRCIBgUAA+NN5BP6mj2DYff35gk6nA61WCzBn2JxO5wPM7/fLz4vD0E+OECfn8xl/0Gw2KbLxeAyLxQIsFgt8p75pDSO7h/HbpUWpewCike9WLpfB7XaDy+WCYrFI/slk8i0MnRRAUt46hPMI4vE4+Hw+ec7t9/44VgWigEeby+UgFArJWjUYOqhWG6x50rpcSfR6PVUfNOgEVRlTX0HhrZBKz4MZjUYWi8VoA+lc9H/VaRZYjBKrtXR8tlwumcFgeMWRbZpA9ORQWfVm8A/FsrLaxebd5wAAAABJRU5ErkJggg=='; + "iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAKsGlDQ1BJQ0MgUHJvZmlsZQAASImVlwdUU+kSgOfe9JDQEiIgJfQmSCeAlBBaAAXpYCMkAUKJMRBU7MriClZURLCs6KqIgo0idizYFsWC3QVZBNR1sWDDlXeBQ9jdd9575805c+a7c+efmf+e/z9nLgCdKZDJMlF1gCxpjjwyyI8dn5DIJvUABRiY0kBdIMyWcSMiwgCTUft3+dgGyJC9YzuU69/f/1fREImzhQBIBMbJomxhFsbHMe0TyuQ5ALg9mN9kbo5siK9gzJRjDWL8ZIhTR7hviJOHGY8fjomO5GGsDUCmCQTyVACaKeZn5wpTsTw0f4ztpSKJFGPsGbyzsmaLMMbqgiUWI8N4KD8n+S95Uv+WM1mZUyBIVfLIXoaF7C/JlmUK5v+fn+N/S1amYrSGOaa0NHlwJGaxvpAHGbNDlSxNnhI+yhLRcPwwpymCY0ZZmM1LHGWRwD9UuTZzStgop0gC+co8OfzoURZnB0SNsnx2pLJWipzHHWWBfKyuIiNG6U8T85X589Ki40Y5VxI7ZZSzM6JCx2J4Sr9cEansXywN8hurG6jce1b2X/Yr4SvX5qRFByv3LhjrXyzljuXMjlf2JhL7B4zFxCjjZTl+ylqyzAhlvDgzSOnPzo1Srs3BDuTY2gjlN0wXhESMMoRBELAhBjIhB+QggECQgBTEOeJ5Q2cUeLNl8+WS1LQcNhe7ZWI2Xyq0m8B2tHd0Bhi6syNH4j1r+C4irGtjvhWVAF4nBgcHT475Qm4BHEkCoNaO+SxnAKh3A1w5JVTIc0d8Q9cJCEAFNWCCDhiACViCLTiCK3iCLwRACIRDNCTATBBCGmRhnc+FhbAMCqAI1sNmKIOdsBv2wyE4CvVwCs7DZbgOt+AePIZ26IJX0AcfYQBBEBJCRxiIDmKImCE2iCPCQbyRACQMiUQSkCQkFZEiCmQhsgIpQoqRMmQXUokcQU4g55GrSCvyEOlAepF3yFcUh9JQJqqPmqMTUQ7KRUPRaHQGmorOQfPQfHQtWopWoAfROvQ8eh29h7ajr9B+HOBUcCycEc4Wx8HxcOG4RFwKTo5bjCvEleAqcNW4Rlwz7g6uHfca9wVPxDPwbLwt3hMfjI/BC/Fz8Ivxq/Fl+P34OvxF/B18B74P/51AJ+gRbAgeBD4hnpBKmEsoIJQQ9hJqCZcI9whdhI9EIpFFtCC6EYOJCcR04gLiauJ2Yg3xHLGV2EnsJ5FIOiQbkhcpnCQg5ZAKSFtJB0lnSbdJXaTPZBWyIdmRHEhOJEvJy8kl5APkM+Tb5G7yAEWdYkbxoIRTRJT5lHWUPZRGyk1KF2WAqkG1oHpRo6np1GXUUmo19RL1CfW9ioqKsYq7ylQVicpSlVKVwypXVDpUvtA0adY0Hm06TUFbS9tHO0d7SHtPp9PN6b70RHoOfS29kn6B/oz+WZWhaqfKVxWpLlEtV61Tva36Ro2iZqbGVZuplqdWonZM7abaa3WKurk6T12gvli9XP2E+n31fg2GhoNGuEaWxmqNAxpXNXo0SZrmmgGaIs18zd2aFzQ7GTiGCYPHEDJWMPYwLjG6mESmBZPPTGcWMQ8xW5h9WppazlqxWvO0yrVOa7WzcCxzFp+VyVrHOspqY30dpz+OO048btW46nG3x33SHq/tqy3WLtSu0b6n/VWHrROgk6GzQade56kuXtdad6ruXN0dupd0X49njvccLxxfOP7o+Ed6qJ61XqTeAr3dejf0+vUN9IP0Zfpb9S/ovzZgGfgapBtsMjhj0GvIMPQ2lBhuMjxr+JKtxeayM9ml7IvsPiM9o2AjhdEuoxajAWML4xjj5cY1xk9NqCYckxSTTSZNJn2mhqaTTReaVpk+MqOYcczSzLaYNZt9MrcwjzNfaV5v3mOhbcG3yLOosnhiSbf0sZxjWWF514poxbHKsNpudcsatXaxTrMut75pg9q42khsttu0TiBMcJ8gnVAx4b4tzZZrm2tbZdthx7ILs1tuV2/3ZqLpxMSJGyY2T/xu72Kfab/H/rGDpkOIw3KHRod3jtaOQsdyx7tOdKdApyVODU5vnW2cxc47nB+4MFwmu6x0aXL509XNVe5a7drrZuqW5LbN7T6HyYngrOZccSe4+7kvcT/l/sXD1SPH46jHH562nhmeBzx7JllMEk/aM6nTy9hL4LXLq92b7Z3k/ZN3u4+Rj8Cnwue5r4mvyHevbzfXipvOPch942fvJ/er9fvE8+At4p3zx/kH+Rf6twRoBsQElAU8CzQOTA2sCuwLcglaEHQumBAcGrwh+D5fny/kV/L7QtxCFoVcDKWFRoWWhT4Psw6ThzVORieHTN44+ckUsynSKfXhEM4P3xj+NMIiYk7EyanEqRFTy6e+iHSIXBjZHMWImhV1IOpjtF/0uujHMZYxipimWLXY6bGVsZ/i/OOK49rjJ8Yvir+eoJsgSWhIJCXGJu5N7J8WMG3ztK7pLtMLprfNsJgxb8bVmbozM2eenqU2SzDrWBIhKS7pQNI3QbigQtCfzE/eltwn5Am3CF+JfEWbRL1iL3GxuDvFK6U4pSfVK3Vjam+aT1pJ2msJT1ImeZsenL4z/VNGeMa+jMHMuMyaLHJWUtYJqaY0Q3pxtsHsebNbZTayAln7HI85m+f0yUPle7OR7BnZDTlMbDi6obBU/KDoyPXOLc/9PDd27rF5GvOk827Mt56/an53XmDezwvwC4QLmhYaLVy2sGMRd9Guxcji5MVNS0yW5C/pWhq0dP8y6rKMZb8st19evPzDirgVjfn6+UvzO38I+qGqQLVAXnB/pefKnT/if5T82LLKadXWVd8LRYXXiuyLSoq+rRauvrbGYU3pmsG1KWtb1rmu27GeuF66vm2Dz4b9xRrFecWdGydvrNvE3lS46cPmWZuvljiX7NxC3aLY0l4aVtqw1XTr+q3fytLK7pX7ldds09u2atun7aLtt3f47qjeqb+zaOfXnyQ/PdgVtKuuwryiZDdxd+7uF3ti9zT/zPm5cq/u3qK9f+6T7mvfH7n/YqVbZeUBvQPrqtAqRVXvwekHbx3yP9RQbVu9q4ZVU3QYDisOvzySdKTtaOjRpmOcY9XHzY5vq2XUFtYhdfPr+urT6tsbEhpaT4ScaGr0bKw9aXdy3ymjU+WntU6vO0M9k39m8Gze2f5zsnOvz6ee72ya1fT4QvyFuxenXmy5FHrpyuXAyxeauc1nr3hdOXXV4+qJa5xr9dddr9fdcLlR+4vLL7Utri11N91uNtzyv9XYOqn1zG2f2+fv+N+5fJd/9/q9Kfda22LaHtyffr/9gehBz8PMh28f5T4aeLz0CeFJ4VP1pyXP9J5V/Gr1a027a/vpDv+OG8+jnj/uFHa++i37t29d+S/oL0q6Dbsrexx7TvUG9t56Oe1l1yvZq4HXBb9r/L7tjeWb43/4/nGjL76v66387eC71e913u/74PyhqT+i/9nHrI8Dnwo/63ze/4Xzpflr3NfugbnfSN9K/7T6s/F76Pcng1mDgzKBXDA8CuAwRVNSAN7tA6AnADCwGYI6bWSmHhZk5D9gmOA/8cjcPSyuANWYGRqNeOcADmNqvhRAzRdgaCyK9gXUyUmpo/Pv8Kw+JAbYv8K0HECi2x6tebQU/iEjc/xf+v6nBWXWv9l/AV0EC6JTIblRAAAAeGVYSWZNTQAqAAAACAAFARIAAwAAAAEAAQAAARoABQAAAAEAAABKARsABQAAAAEAAABSASgAAwAAAAEAAgAAh2kABAAAAAEAAABaAAAAAAAAAJAAAAABAAAAkAAAAAEAAqACAAQAAAABAAAAFKADAAQAAAABAAAAFAAAAAAXNii1AAAACXBIWXMAABYlAAAWJQFJUiTwAAAB82lUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNi4wLjAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyI+CiAgICAgICAgIDx0aWZmOllSZXNvbHV0aW9uPjE0NDwvdGlmZjpZUmVzb2x1dGlvbj4KICAgICAgICAgPHRpZmY6T3JpZW50YXRpb24+MTwvdGlmZjpPcmllbnRhdGlvbj4KICAgICAgICAgPHRpZmY6WFJlc29sdXRpb24+MTQ0PC90aWZmOlhSZXNvbHV0aW9uPgogICAgICAgICA8dGlmZjpSZXNvbHV0aW9uVW5pdD4yPC90aWZmOlJlc29sdXRpb25Vbml0PgogICAgICA8L3JkZjpEZXNjcmlwdGlvbj4KICAgPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4KReh49gAAAjRJREFUOBGFlD2vMUEUx2clvoNCcW8hCqFAo1dKhEQpvsF9KrWEBh/ALbQ0KkInBI3SWyGPCCJEQliXgsTLefaca/bBWjvJzs6cOf/fnDkzOQJIjWm06/XKBEGgD8c6nU5VIWgBtQDPZPWtJE8O63a7LBgMMo/Hw0ql0jPjcY4RvmqXy4XMjUYDUwLtdhtmsxnYbDbI5/O0djqdFFKmsEiGZ9jP9gem0yn0ej2Yz+fg9XpfycimAD7DttstQTDKfr8Po9GIIg6Hw1Cr1RTgB+A72GAwgMPhQLBMJgNSXsFqtUI2myUo18pA6QJogefsPrLBX4QdCVatViklw+EQRFGEj88P2O12pEUGATmsXq+TaLPZ0AXgMRF2vMEqlQoJTSYTpNNpApvNZliv1/+BHDaZTAi2Wq1A3Ig0xmMej7+RcZjdbodUKkWAaDQK+GHjHPnImB88JrZIJAKFQgH2+z2BOczhcMiwRCIBgUAA+NN5BP6mj2DYff35gk6nA61WCzBn2JxO5wPM7/fLz4vD0E+OECfn8xl/0Gw2KbLxeAyLxQIsFgt8p75pDSO7h/HbpUWpewCike9WLpfB7XaDy+WCYrFI/slk8i0MnRRAUt46hPMI4vE4+Hw+ec7t9/44VgWigEeby+UgFArJWjUYOqhWG6x50rpcSfR6PVUfNOgEVRlTX0HhrZBKz4MZjUYWi8VoA+lc9H/VaRZYjBKrtXR8tlwumcFgeMWRbZpA9ORQWfVm8A/FsrLaxebd5wAAAABJRU5ErkJggg=="; // Tool configuration -const name = 'get-tiny-image'; +const name = "get-tiny-image"; const config = { - title: 'Get Tiny Image Tool', - description: 'Returns a tiny MCP logo image.', - inputSchema: {}, - annotations: { - readOnlyHint: true, - destructiveHint: false, - idempotentHint: true, - openWorldHint: false - } + title: "Get Tiny Image Tool", + description: "Returns a tiny MCP logo image.", + inputSchema: {}, + annotations: { + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: false, + }, }; /** @@ -30,23 +30,23 @@ const config = { * @param server - The McpServer instance where the tool will be registered. */ export const registerGetTinyImageTool = (server: McpServer) => { - server.registerTool(name, config, async (args): Promise => { - return { - content: [ - { - type: 'text', - text: "Here's the image you requested:" - }, - { - type: 'image', - data: MCP_TINY_IMAGE, - mimeType: 'image/png' - }, - { - type: 'text', - text: 'The image above is the MCP logo.' - } - ] - }; - }); + server.registerTool(name, config, async (args): Promise => { + return { + content: [ + { + type: "text", + text: "Here's the image you requested:", + }, + { + type: "image", + data: MCP_TINY_IMAGE, + mimeType: "image/png", + }, + { + type: "text", + text: "The image above is the MCP logo.", + }, + ], + }; + }); }; diff --git a/src/everything/tools/gzip-file-as-resource.ts b/src/everything/tools/gzip-file-as-resource.ts index 00e0002ce6..148a9afea8 100644 --- a/src/everything/tools/gzip-file-as-resource.ts +++ b/src/everything/tools/gzip-file-as-resource.ts @@ -1,48 +1,61 @@ -import { z } from 'zod'; -import { McpServer, CallToolResult, Resource } from '@modelcontextprotocol/server'; -import { gzipSync } from 'node:zlib'; -import { getSessionResourceURI, registerSessionResource } from '../resources/session.js'; +import { z } from "zod"; +import { + McpServer, + CallToolResult, + Resource, +} from "@modelcontextprotocol/server"; +import { gzipSync } from "node:zlib"; +import { + getSessionResourceURI, + registerSessionResource, +} from "../resources/session.js"; // Maximum input file size - 10 MB default -const GZIP_MAX_FETCH_SIZE = Number(process.env.GZIP_MAX_FETCH_SIZE ?? String(10 * 1024 * 1024)); +const GZIP_MAX_FETCH_SIZE = Number( + process.env.GZIP_MAX_FETCH_SIZE ?? String(10 * 1024 * 1024) +); // Maximum fetch time - 30 seconds default. -const GZIP_MAX_FETCH_TIME_MILLIS = Number(process.env.GZIP_MAX_FETCH_TIME_MILLIS ?? String(30 * 1000)); +const GZIP_MAX_FETCH_TIME_MILLIS = Number( + process.env.GZIP_MAX_FETCH_TIME_MILLIS ?? String(30 * 1000) +); // Comma-separated list of allowed domains. Empty means all domains are allowed. -const GZIP_ALLOWED_DOMAINS = (process.env.GZIP_ALLOWED_DOMAINS ?? '') - .split(',') - .map(d => d.trim().toLowerCase()) - .filter(d => d.length > 0); +const GZIP_ALLOWED_DOMAINS = (process.env.GZIP_ALLOWED_DOMAINS ?? "") + .split(",") + .map((d) => d.trim().toLowerCase()) + .filter((d) => d.length > 0); // Tool input schema const GZipFileAsResourceSchema = z.object({ - name: z.string().describe('Name of the output file').default('README.md.gz'), - data: z - .url() - .describe('URL or data URI of the file content to compress') - .default('https://raw.githubusercontent.com/modelcontextprotocol/servers/refs/heads/main/README.md'), - outputType: z - .enum(['resourceLink', 'resource']) - .default('resourceLink') - .describe( - "How the resulting gzipped file should be returned. 'resourceLink' returns a link to a resource that can be read later, 'resource' returns a full resource object." - ) + name: z.string().describe("Name of the output file").default("README.md.gz"), + data: z + .url() + .describe("URL or data URI of the file content to compress") + .default( + "https://raw.githubusercontent.com/modelcontextprotocol/servers/refs/heads/main/README.md" + ), + outputType: z + .enum(["resourceLink", "resource"]) + .default("resourceLink") + .describe( + "How the resulting gzipped file should be returned. 'resourceLink' returns a link to a resource that can be read later, 'resource' returns a full resource object." + ), }); // Tool configuration -const name = 'gzip-file-as-resource'; +const name = "gzip-file-as-resource"; const config = { - title: 'GZip File as Resource Tool', - description: - 'Compresses a single file using gzip compression. Depending upon the selected output type, returns either the compressed data as a gzipped resource or a resource link, allowing it to be downloaded in a subsequent request during the current session.', - inputSchema: GZipFileAsResourceSchema, - annotations: { - readOnlyHint: false, - destructiveHint: false, - idempotentHint: true, - openWorldHint: true - } + title: "GZip File as Resource Tool", + description: + "Compresses a single file using gzip compression. Depending upon the selected output type, returns either the compressed data as a gzipped resource or a resource link, allowing it to be downloaded in a subsequent request during the current session.", + inputSchema: GZipFileAsResourceSchema, + annotations: { + readOnlyHint: false, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true, + }, }; /** @@ -61,49 +74,58 @@ const config = { * @throws {Error} Throws an error if an unknown output type is specified. */ export const registerGZipFileAsResourceTool = (server: McpServer) => { - server.registerTool(name, config, async (args): Promise => { - const { name, data: dataUri, outputType } = GZipFileAsResourceSchema.parse(args); - - // Validate data uri - const url = validateDataURI(dataUri); - - // Fetch the data - const response = await fetchSafely(url, { - maxBytes: GZIP_MAX_FETCH_SIZE, - timeoutMillis: GZIP_MAX_FETCH_TIME_MILLIS - }); - - // Compress the data using gzip - const inputBuffer = Buffer.from(response); - const compressedBuffer = gzipSync(inputBuffer); - - // Create resource - const uri = getSessionResourceURI(name); - const blob = compressedBuffer.toString('base64'); - const mimeType = 'application/gzip'; - const resource = { uri, name, mimeType }; - - // Register resource, get resource link in return - const resourceLink = registerSessionResource(server, resource, 'blob', blob); - - // Return the resource or a resource link that can be used to access this resource later - if (outputType === 'resource') { - return { - content: [ - { - type: 'resource', - resource: { uri, mimeType, blob } - } - ] - }; - } else if (outputType === 'resourceLink') { - return { - content: [resourceLink] - }; - } else { - throw new Error(`Unknown outputType: ${outputType}`); - } + server.registerTool(name, config, async (args): Promise => { + const { + name, + data: dataUri, + outputType, + } = GZipFileAsResourceSchema.parse(args); + + // Validate data uri + const url = validateDataURI(dataUri); + + // Fetch the data + const response = await fetchSafely(url, { + maxBytes: GZIP_MAX_FETCH_SIZE, + timeoutMillis: GZIP_MAX_FETCH_TIME_MILLIS, }); + + // Compress the data using gzip + const inputBuffer = Buffer.from(response); + const compressedBuffer = gzipSync(inputBuffer); + + // Create resource + const uri = getSessionResourceURI(name); + const blob = compressedBuffer.toString("base64"); + const mimeType = "application/gzip"; + const resource = { uri, name, mimeType }; + + // Register resource, get resource link in return + const resourceLink = registerSessionResource( + server, + resource, + "blob", + blob + ); + + // Return the resource or a resource link that can be used to access this resource later + if (outputType === "resource") { + return { + content: [ + { + type: "resource", + resource: { uri, mimeType, blob }, + }, + ], + }; + } else if (outputType === "resourceLink") { + return { + content: [resourceLink], + }; + } else { + throw new Error(`Unknown outputType: ${outputType}`); + } + }); }; /** @@ -114,25 +136,38 @@ export const registerGZipFileAsResourceTool = (server: McpServer) => { * @throws {Error} If the data URI does not use a supported protocol or does not meet allowed domains criteria. */ function validateDataURI(dataUri: string): URL { - // Validate Inputs - const url = new URL(dataUri); - try { - if (url.protocol !== 'http:' && url.protocol !== 'https:' && url.protocol !== 'data:') { - throw new Error(`Unsupported URL protocol for ${dataUri}. Only http, https, and data URLs are supported.`); - } - if (GZIP_ALLOWED_DOMAINS.length > 0 && (url.protocol === 'http:' || url.protocol === 'https:')) { - const domain = url.hostname; - const domainAllowed = GZIP_ALLOWED_DOMAINS.some(allowedDomain => { - return domain === allowedDomain || domain.endsWith(`.${allowedDomain}`); - }); - if (!domainAllowed) { - throw new Error(`Domain ${domain} is not in the allowed domains list.`); - } - } - } catch (error) { - throw new Error(`Error processing file ${dataUri}: ${error instanceof Error ? error.message : String(error)}`); + // Validate Inputs + const url = new URL(dataUri); + try { + if ( + url.protocol !== "http:" && + url.protocol !== "https:" && + url.protocol !== "data:" + ) { + throw new Error( + `Unsupported URL protocol for ${dataUri}. Only http, https, and data URLs are supported.` + ); + } + if ( + GZIP_ALLOWED_DOMAINS.length > 0 && + (url.protocol === "http:" || url.protocol === "https:") + ) { + const domain = url.hostname; + const domainAllowed = GZIP_ALLOWED_DOMAINS.some((allowedDomain) => { + return domain === allowedDomain || domain.endsWith(`.${allowedDomain}`); + }); + if (!domainAllowed) { + throw new Error(`Domain ${domain} is not in the allowed domains list.`); + } } - return url; + } catch (error) { + throw new Error( + `Error processing file ${dataUri}: ${ + error instanceof Error ? error.message : String(error) + }` + ); + } + return url; } /** @@ -145,64 +180,72 @@ function validateDataURI(dataUri: string): URL { * @return {Promise} A promise that resolves with the response as an ArrayBuffer if successful. * @throws {Error} Throws an error if the response size exceeds the defined limit, the fetch times out, or the response is otherwise invalid. */ -async function fetchSafely(url: URL, { maxBytes, timeoutMillis }: { maxBytes: number; timeoutMillis: number }): Promise { - const controller = new AbortController(); - const timeout = setTimeout( - () => controller.abort(`Fetching ${url} took more than ${timeoutMillis} ms and was aborted.`), - timeoutMillis - ); - - try { - // Fetch the data - const response = await fetch(url, { signal: controller.signal }); - if (!response.body) { - throw new Error('No response body'); - } - - // Note: we can't trust the Content-Length header: a malicious or clumsy server could return much more data than advertised. - // We check it here for early bail-out, but we still need to monitor actual bytes read below. - const contentLengthHeader = response.headers.get('content-length'); - if (contentLengthHeader != null) { - const contentLength = parseInt(contentLengthHeader, 10); - if (contentLength > maxBytes) { - throw new Error(`Content-Length for ${url} exceeds max of ${maxBytes}: ${contentLength}`); - } - } - - // Read the fetched data from the response body - const reader = response.body.getReader(); - const chunks = []; - let totalSize = 0; +async function fetchSafely( + url: URL, + { maxBytes, timeoutMillis }: { maxBytes: number; timeoutMillis: number } +): Promise { + const controller = new AbortController(); + const timeout = setTimeout( + () => + controller.abort( + `Fetching ${url} took more than ${timeoutMillis} ms and was aborted.` + ), + timeoutMillis + ); + + try { + // Fetch the data + const response = await fetch(url, { signal: controller.signal }); + if (!response.body) { + throw new Error("No response body"); + } - // Read chunks until done - try { - while (true) { - const { done, value } = await reader.read(); - if (done) break; + // Note: we can't trust the Content-Length header: a malicious or clumsy server could return much more data than advertised. + // We check it here for early bail-out, but we still need to monitor actual bytes read below. + const contentLengthHeader = response.headers.get("content-length"); + if (contentLengthHeader != null) { + const contentLength = parseInt(contentLengthHeader, 10); + if (contentLength > maxBytes) { + throw new Error( + `Content-Length for ${url} exceeds max of ${maxBytes}: ${contentLength}` + ); + } + } - totalSize += value.length; + // Read the fetched data from the response body + const reader = response.body.getReader(); + const chunks = []; + let totalSize = 0; - if (totalSize > maxBytes) { - reader.cancel(); - throw new Error(`Response from ${url} exceeds ${maxBytes} bytes`); - } + // Read chunks until done + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; - chunks.push(value); - } - } finally { - reader.releaseLock(); - } + totalSize += value.length; - // Combine chunks into a single buffer - const buffer = new Uint8Array(totalSize); - let offset = 0; - for (const chunk of chunks) { - buffer.set(chunk, offset); - offset += chunk.length; + if (totalSize > maxBytes) { + reader.cancel(); + throw new Error(`Response from ${url} exceeds ${maxBytes} bytes`); } - return buffer.buffer; + chunks.push(value); + } } finally { - clearTimeout(timeout); + reader.releaseLock(); } + + // Combine chunks into a single buffer + const buffer = new Uint8Array(totalSize); + let offset = 0; + for (const chunk of chunks) { + buffer.set(chunk, offset); + offset += chunk.length; + } + + return buffer.buffer; + } finally { + clearTimeout(timeout); + } } diff --git a/src/everything/tools/index.ts b/src/everything/tools/index.ts index c6dd555208..ded05e027e 100644 --- a/src/everything/tools/index.ts +++ b/src/everything/tools/index.ts @@ -1,40 +1,40 @@ -import { McpServer } from '@modelcontextprotocol/server'; -import { registerGetAnnotatedMessageTool } from './get-annotated-message.js'; -import { registerEchoTool } from './echo.js'; -import { registerGetEnvTool } from './get-env.js'; -import { registerGetResourceLinksTool } from './get-resource-links.js'; -import { registerGetResourceReferenceTool } from './get-resource-reference.js'; -import { registerGetRootsListTool } from './get-roots-list.js'; -import { registerGetStructuredContentTool } from './get-structured-content.js'; -import { registerGetSumTool } from './get-sum.js'; -import { registerGetTinyImageTool } from './get-tiny-image.js'; -import { registerGZipFileAsResourceTool } from './gzip-file-as-resource.js'; -import { registerToggleSimulatedLoggingTool } from './toggle-simulated-logging.js'; -import { registerToggleSubscriberUpdatesTool } from './toggle-subscriber-updates.js'; -import { registerTriggerElicitationRequestTool } from './trigger-elicitation-request.js'; -import { registerTriggerLongRunningOperationTool } from './trigger-long-running-operation.js'; -import { registerTriggerSamplingRequestTool } from './trigger-sampling-request.js'; -import { registerTriggerSamplingRequestAsyncTool } from './trigger-sampling-request-async.js'; -import { registerTriggerElicitationRequestAsyncTool } from './trigger-elicitation-request-async.js'; -import { registerTriggerUrlElicitationTool } from './trigger-url-elicitation.js'; +import { McpServer } from "@modelcontextprotocol/server"; +import { registerGetAnnotatedMessageTool } from "./get-annotated-message.js"; +import { registerEchoTool } from "./echo.js"; +import { registerGetEnvTool } from "./get-env.js"; +import { registerGetResourceLinksTool } from "./get-resource-links.js"; +import { registerGetResourceReferenceTool } from "./get-resource-reference.js"; +import { registerGetRootsListTool } from "./get-roots-list.js"; +import { registerGetStructuredContentTool } from "./get-structured-content.js"; +import { registerGetSumTool } from "./get-sum.js"; +import { registerGetTinyImageTool } from "./get-tiny-image.js"; +import { registerGZipFileAsResourceTool } from "./gzip-file-as-resource.js"; +import { registerToggleSimulatedLoggingTool } from "./toggle-simulated-logging.js"; +import { registerToggleSubscriberUpdatesTool } from "./toggle-subscriber-updates.js"; +import { registerTriggerElicitationRequestTool } from "./trigger-elicitation-request.js"; +import { registerTriggerLongRunningOperationTool } from "./trigger-long-running-operation.js"; +import { registerTriggerSamplingRequestTool } from "./trigger-sampling-request.js"; +import { registerTriggerSamplingRequestAsyncTool } from "./trigger-sampling-request-async.js"; +import { registerTriggerElicitationRequestAsyncTool } from "./trigger-elicitation-request-async.js"; +import { registerTriggerUrlElicitationTool } from "./trigger-url-elicitation.js"; /** * Register the tools with the MCP server. * @param server */ export const registerTools = (server: McpServer) => { - registerEchoTool(server); - registerGetAnnotatedMessageTool(server); - registerGetEnvTool(server); - registerGetResourceLinksTool(server); - registerGetResourceReferenceTool(server); - registerGetStructuredContentTool(server); - registerGetSumTool(server); - registerGetTinyImageTool(server); - registerGZipFileAsResourceTool(server); - registerToggleSimulatedLoggingTool(server); - registerToggleSubscriberUpdatesTool(server); - registerTriggerLongRunningOperationTool(server); + registerEchoTool(server); + registerGetAnnotatedMessageTool(server); + registerGetEnvTool(server); + registerGetResourceLinksTool(server); + registerGetResourceReferenceTool(server); + registerGetStructuredContentTool(server); + registerGetSumTool(server); + registerGetTinyImageTool(server); + registerGZipFileAsResourceTool(server); + registerToggleSimulatedLoggingTool(server); + registerToggleSubscriberUpdatesTool(server); + registerTriggerLongRunningOperationTool(server); }; /** @@ -42,11 +42,11 @@ export const registerTools = (server: McpServer) => { * These must be registered conditionally, after initialization. */ export const registerConditionalTools = (server: McpServer) => { - registerGetRootsListTool(server); - registerTriggerElicitationRequestTool(server); - registerTriggerUrlElicitationTool(server); - registerTriggerSamplingRequestTool(server); - // Bidirectional task tools - server sends requests that client executes as tasks - registerTriggerSamplingRequestAsyncTool(server); - registerTriggerElicitationRequestAsyncTool(server); + registerGetRootsListTool(server); + registerTriggerElicitationRequestTool(server); + registerTriggerUrlElicitationTool(server); + registerTriggerSamplingRequestTool(server); + // Bidirectional task tools - server sends requests that client executes as tasks + registerTriggerSamplingRequestAsyncTool(server); + registerTriggerElicitationRequestAsyncTool(server); }; diff --git a/src/everything/tools/toggle-simulated-logging.ts b/src/everything/tools/toggle-simulated-logging.ts index 480c72ca0b..6397516217 100644 --- a/src/everything/tools/toggle-simulated-logging.ts +++ b/src/everything/tools/toggle-simulated-logging.ts @@ -1,18 +1,21 @@ -import { McpServer, CallToolResult } from '@modelcontextprotocol/server'; -import { beginSimulatedLogging, stopSimulatedLogging } from '../server/logging.js'; +import { McpServer, CallToolResult } from "@modelcontextprotocol/server"; +import { + beginSimulatedLogging, + stopSimulatedLogging, +} from "../server/logging.js"; // Tool configuration -const name = 'toggle-simulated-logging'; +const name = "toggle-simulated-logging"; const config = { - title: 'Toggle Simulated Logging', - description: 'Toggles simulated, random-leveled logging on or off.', - inputSchema: {}, - annotations: { - readOnlyHint: false, - destructiveHint: false, - idempotentHint: false, - openWorldHint: false - } + title: "Toggle Simulated Logging", + description: "Toggles simulated, random-leveled logging on or off.", + inputSchema: {}, + annotations: { + readOnlyHint: false, + destructiveHint: false, + idempotentHint: false, + openWorldHint: false, + }, }; // Track enabled clients by session id @@ -31,22 +34,26 @@ const clients: Set = new Set(); * @param {McpServer} server - The McpServer instance where the tool will be registered. */ export const registerToggleSimulatedLoggingTool = (server: McpServer) => { - server.registerTool(name, config, async (_args, ctx): Promise => { - const sessionId = ctx?.sessionId; + server.registerTool( + name, + config, + async (_args, ctx): Promise => { + const sessionId = ctx?.sessionId; - let response: string; - if (clients.has(sessionId)) { - stopSimulatedLogging(sessionId); - clients.delete(sessionId); - response = `Stopped simulated logging for session ${sessionId}`; - } else { - beginSimulatedLogging(server, sessionId); - clients.add(sessionId); - response = `Started simulated, random-leveled logging for session ${sessionId} at a 5 second pace. Client's selected logging level will be respected. If an interval elapses and the message to be sent is below the selected level, it will not be sent. Thus at higher chosen logging levels, messages should arrive further apart. `; - } + let response: string; + if (clients.has(sessionId)) { + stopSimulatedLogging(sessionId); + clients.delete(sessionId); + response = `Stopped simulated logging for session ${sessionId}`; + } else { + beginSimulatedLogging(server, sessionId); + clients.add(sessionId); + response = `Started simulated, random-leveled logging for session ${sessionId} at a 5 second pace. Client's selected logging level will be respected. If an interval elapses and the message to be sent is below the selected level, it will not be sent. Thus at higher chosen logging levels, messages should arrive further apart. `; + } - return { - content: [{ type: 'text', text: `${response}` }] - }; - }); + return { + content: [{ type: "text", text: `${response}` }], + }; + } + ); }; diff --git a/src/everything/tools/toggle-subscriber-updates.ts b/src/everything/tools/toggle-subscriber-updates.ts index 68fcd7f634..bc2f5dea7a 100644 --- a/src/everything/tools/toggle-subscriber-updates.ts +++ b/src/everything/tools/toggle-subscriber-updates.ts @@ -1,18 +1,21 @@ -import { McpServer, CallToolResult } from '@modelcontextprotocol/server'; -import { beginSimulatedResourceUpdates, stopSimulatedResourceUpdates } from '../resources/subscriptions.js'; +import { McpServer, CallToolResult } from "@modelcontextprotocol/server"; +import { + beginSimulatedResourceUpdates, + stopSimulatedResourceUpdates, +} from "../resources/subscriptions.js"; // Tool configuration -const name = 'toggle-subscriber-updates'; +const name = "toggle-subscriber-updates"; const config = { - title: 'Toggle Subscriber Updates', - description: 'Toggles simulated resource subscription updates on or off.', - inputSchema: {}, - annotations: { - readOnlyHint: false, - destructiveHint: false, - idempotentHint: false, - openWorldHint: false - } + title: "Toggle Subscriber Updates", + description: "Toggles simulated resource subscription updates on or off.", + inputSchema: {}, + annotations: { + readOnlyHint: false, + destructiveHint: false, + idempotentHint: false, + openWorldHint: false, + }, }; // Track enabled clients by session id @@ -34,22 +37,26 @@ const clients: Set = new Set(); * @param {McpServer} server - The McpServer instance where the tool will be registered. */ export const registerToggleSubscriberUpdatesTool = (server: McpServer) => { - server.registerTool(name, config, async (_args, ctx): Promise => { - const sessionId = ctx?.sessionId; + server.registerTool( + name, + config, + async (_args, ctx): Promise => { + const sessionId = ctx?.sessionId; - let response: string; - if (clients.has(sessionId)) { - stopSimulatedResourceUpdates(sessionId); - clients.delete(sessionId); - response = `Stopped simulated resource updates for session ${sessionId}`; - } else { - beginSimulatedResourceUpdates(server, sessionId); - clients.add(sessionId); - response = `Started simulated resource updated notifications for session ${sessionId} at a 5 second pace. Client will receive updates for any resources the it is subscribed to.`; - } + let response: string; + if (clients.has(sessionId)) { + stopSimulatedResourceUpdates(sessionId); + clients.delete(sessionId); + response = `Stopped simulated resource updates for session ${sessionId}`; + } else { + beginSimulatedResourceUpdates(server, sessionId); + clients.add(sessionId); + response = `Started simulated resource updated notifications for session ${sessionId} at a 5 second pace. Client will receive updates for any resources the it is subscribed to.`; + } - return { - content: [{ type: 'text', text: `${response}` }] - }; - }); + return { + content: [{ type: "text", text: `${response}` }], + }; + } + ); }; diff --git a/src/everything/tools/trigger-elicitation-request-async.ts b/src/everything/tools/trigger-elicitation-request-async.ts index 8b50c1c5d6..1e208c0f47 100644 --- a/src/everything/tools/trigger-elicitation-request-async.ts +++ b/src/everything/tools/trigger-elicitation-request-async.ts @@ -1,21 +1,21 @@ -import { McpServer, CallToolResult } from '@modelcontextprotocol/server'; -import { z } from 'zod'; +import { McpServer, CallToolResult } from "@modelcontextprotocol/server"; +import { z } from "zod"; // Tool configuration -const name = 'trigger-elicitation-request-async'; +const name = "trigger-elicitation-request-async"; const config = { - title: 'Trigger Async Elicitation Request Tool', - description: - 'Trigger an async elicitation request that the CLIENT executes as a background task. ' + - 'Demonstrates bidirectional MCP tasks where the server sends an elicitation request and ' + - 'the client handles user input asynchronously, allowing the server to poll for completion.', - inputSchema: {}, - annotations: { - readOnlyHint: false, - destructiveHint: false, - idempotentHint: false, - openWorldHint: false - } + title: "Trigger Async Elicitation Request Tool", + description: + "Trigger an async elicitation request that the CLIENT executes as a background task. " + + "Demonstrates bidirectional MCP tasks where the server sends an elicitation request and " + + "the client handles user input asynchronously, allowing the server to poll for completion.", + inputSchema: {}, + annotations: { + readOnlyHint: false, + destructiveHint: false, + idempotentHint: false, + openWorldHint: false, + }, }; // Poll interval in milliseconds @@ -36,203 +36,233 @@ const MAX_POLL_ATTEMPTS = 600; * * @param {McpServer} server - The McpServer instance where the tool will be registered. */ -export const registerTriggerElicitationRequestAsyncTool = (server: McpServer) => { - // Check client capabilities - const clientCapabilities = server.server.getClientCapabilities() || {}; - - // Client must support elicitation AND tasks.requests.elicitation - const clientSupportsElicitation = clientCapabilities.elicitation !== undefined; - const clientTasksCapability = clientCapabilities.tasks as - | { - requests?: { elicitation?: { create?: object } }; - } - | undefined; - const clientSupportsAsyncElicitation = clientTasksCapability?.requests?.elicitation?.create !== undefined; - - if (clientSupportsElicitation && clientSupportsAsyncElicitation) { - server.registerTool(name, config, async (args, ctx): Promise => { - // Create the elicitation request WITH task metadata - // Using z.any() schema to avoid complex type matching with _meta - const request = { - method: 'elicitation/create' as const, - params: { - task: { - ttl: 600000 // 10 minutes (user input may take a while) - }, - message: 'Please provide inputs for the following fields (async task demo):', - requestedSchema: { - type: 'object' as const, - properties: { - name: { - title: 'Your Name', - type: 'string' as const, - description: 'Your full name' - }, - favoriteColor: { - title: 'Favorite Color', - type: 'string' as const, - description: 'What is your favorite color?', - enum: ['Red', 'Blue', 'Green', 'Yellow', 'Purple'] - }, - agreeToTerms: { - title: 'Terms Agreement', - type: 'boolean' as const, - description: 'Do you agree to the terms and conditions?' - } - }, - required: ['name'] - } - } - }; - - // Send the elicitation request - // Client may return either: - // - ElicitResult (synchronous execution) - // - CreateTaskResult (task-based execution with { task } object) - const elicitResponse = await ctx.mcpReq.send( - request as Parameters[0], - z.union([ - // CreateTaskResult - client created a task - z.object({ - task: z.object({ - taskId: z.string(), - status: z.string(), - pollInterval: z.number().optional(), - statusMessage: z.string().optional() - }) - }), - // ElicitResult - synchronous execution - z.object({ - action: z.string(), - content: z.any().optional() - }) - ]) - ); +export const registerTriggerElicitationRequestAsyncTool = ( + server: McpServer +) => { + // Check client capabilities + const clientCapabilities = server.server.getClientCapabilities() || {}; + + // Client must support elicitation AND tasks.requests.elicitation + const clientSupportsElicitation = + clientCapabilities.elicitation !== undefined; + const clientTasksCapability = clientCapabilities.tasks as + | { + requests?: { elicitation?: { create?: object } }; + } + | undefined; + const clientSupportsAsyncElicitation = + clientTasksCapability?.requests?.elicitation?.create !== undefined; - // Check if client returned CreateTaskResult (has task object) - const isTaskResult = 'task' in elicitResponse && elicitResponse.task; - if (!isTaskResult) { - // Client executed synchronously - return the direct response - return { - content: [ - { - type: 'text', - text: `[SYNC] Client executed synchronously:\n${JSON.stringify(elicitResponse, null, 2)}` - } - ] - }; - } - - const taskId = elicitResponse.task.taskId; - const statusMessages: string[] = []; - statusMessages.push(`Task created: ${taskId}`); - - // Poll for task completion - let attempts = 0; - let taskStatus = elicitResponse.task.status; - let taskStatusMessage: string | undefined; - - while (taskStatus !== 'completed' && taskStatus !== 'failed' && taskStatus !== 'cancelled' && attempts < MAX_POLL_ATTEMPTS) { - // Wait before polling - await new Promise(resolve => setTimeout(resolve, POLL_INTERVAL)); - attempts++; - - // Get task status from client - const pollResult = await ctx.mcpReq.send( - { - method: 'tasks/get', - params: { taskId } - }, - z.looseObject({ - status: z.string(), - statusMessage: z.string().optional() - }) - ); - - taskStatus = pollResult.status; - taskStatusMessage = pollResult.statusMessage; - - // Only log status changes or every 10 polls to avoid spam - if (attempts === 1 || attempts % 10 === 0 || taskStatus !== 'input_required') { - statusMessages.push(`Poll ${attempts}: ${taskStatus}${taskStatusMessage ? ` - ${taskStatusMessage}` : ''}`); - } - } - - // Check for timeout - if (attempts >= MAX_POLL_ATTEMPTS) { - return { - content: [ - { - type: 'text', - text: `[TIMEOUT] Task timed out after ${MAX_POLL_ATTEMPTS} poll attempts\n\nProgress:\n${statusMessages.join( - '\n' - )}` - } - ] - }; - } - - // Check for failure/cancellation - if (taskStatus === 'failed' || taskStatus === 'cancelled') { - return { - content: [ - { - type: 'text', - text: `[${taskStatus.toUpperCase()}] ${taskStatusMessage || 'No message'}\n\nProgress:\n${statusMessages.join( - '\n' - )}` - } - ] - }; - } - - // Fetch the final result - const result = await ctx.mcpReq.send( - { - method: 'tasks/result', - params: { taskId } + if (clientSupportsElicitation && clientSupportsAsyncElicitation) { + server.registerTool( + name, + config, + async (args, ctx): Promise => { + // Create the elicitation request WITH task metadata + // Using z.any() schema to avoid complex type matching with _meta + const request = { + method: "elicitation/create" as const, + params: { + task: { + ttl: 600000, // 10 minutes (user input may take a while) + }, + message: + "Please provide inputs for the following fields (async task demo):", + requestedSchema: { + type: "object" as const, + properties: { + name: { + title: "Your Name", + type: "string" as const, + description: "Your full name", }, - z.any() + favoriteColor: { + title: "Favorite Color", + type: "string" as const, + description: "What is your favorite color?", + enum: ["Red", "Blue", "Green", "Yellow", "Purple"], + }, + agreeToTerms: { + title: "Terms Agreement", + type: "boolean" as const, + description: "Do you agree to the terms and conditions?", + }, + }, + required: ["name"], + }, + }, + }; + + // Send the elicitation request + // Client may return either: + // - ElicitResult (synchronous execution) + // - CreateTaskResult (task-based execution with { task } object) + const elicitResponse = await ctx.mcpReq.send( + request as Parameters[0], + z.union([ + // CreateTaskResult - client created a task + z.object({ + task: z.object({ + taskId: z.string(), + status: z.string(), + pollInterval: z.number().optional(), + statusMessage: z.string().optional(), + }), + }), + // ElicitResult - synchronous execution + z.object({ + action: z.string(), + content: z.any().optional(), + }), + ]) + ); + + // Check if client returned CreateTaskResult (has task object) + const isTaskResult = "task" in elicitResponse && elicitResponse.task; + if (!isTaskResult) { + // Client executed synchronously - return the direct response + return { + content: [ + { + type: "text", + text: `[SYNC] Client executed synchronously:\n${JSON.stringify( + elicitResponse, + null, + 2 + )}`, + }, + ], + }; + } + + const taskId = elicitResponse.task.taskId; + const statusMessages: string[] = []; + statusMessages.push(`Task created: ${taskId}`); + + // Poll for task completion + let attempts = 0; + let taskStatus = elicitResponse.task.status; + let taskStatusMessage: string | undefined; + + while ( + taskStatus !== "completed" && + taskStatus !== "failed" && + taskStatus !== "cancelled" && + attempts < MAX_POLL_ATTEMPTS + ) { + // Wait before polling + await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL)); + attempts++; + + // Get task status from client + const pollResult = await ctx.mcpReq.send( + { + method: "tasks/get", + params: { taskId }, + }, + z.looseObject({ + status: z.string(), + statusMessage: z.string().optional(), + }) + ); + + taskStatus = pollResult.status; + taskStatusMessage = pollResult.statusMessage; + + // Only log status changes or every 10 polls to avoid spam + if ( + attempts === 1 || + attempts % 10 === 0 || + taskStatus !== "input_required" + ) { + statusMessages.push( + `Poll ${attempts}: ${taskStatus}${ + taskStatusMessage ? ` - ${taskStatusMessage}` : "" + }` ); + } + } + + // Check for timeout + if (attempts >= MAX_POLL_ATTEMPTS) { + return { + content: [ + { + type: "text", + text: `[TIMEOUT] Task timed out after ${MAX_POLL_ATTEMPTS} poll attempts\n\nProgress:\n${statusMessages.join( + "\n" + )}`, + }, + ], + }; + } + + // Check for failure/cancellation + if (taskStatus === "failed" || taskStatus === "cancelled") { + return { + content: [ + { + type: "text", + text: `[${taskStatus.toUpperCase()}] ${ + taskStatusMessage || "No message" + }\n\nProgress:\n${statusMessages.join("\n")}`, + }, + ], + }; + } + + // Fetch the final result + const result = await ctx.mcpReq.send( + { + method: "tasks/result", + params: { taskId }, + }, + z.any() + ); - // Format the elicitation result - const content: CallToolResult['content'] = []; - - if (result.action === 'accept' && result.content) { - content.push({ - type: 'text', - text: `[COMPLETED] User provided the requested information!` - }); - - const userData = result.content as Record; - const lines = []; - if (userData.name) lines.push(`- Name: ${userData.name}`); - if (userData.favoriteColor) lines.push(`- Favorite Color: ${userData.favoriteColor}`); - if (userData.agreeToTerms !== undefined) lines.push(`- Agreed to terms: ${userData.agreeToTerms}`); - - content.push({ - type: 'text', - text: `User inputs:\n${lines.join('\n')}` - }); - } else if (result.action === 'decline') { - content.push({ - type: 'text', - text: `[DECLINED] User declined to provide the requested information.` - }); - } else if (result.action === 'cancel') { - content.push({ - type: 'text', - text: `[CANCELLED] User cancelled the elicitation dialog.` - }); - } - - // Include progress and raw result for debugging - content.push({ - type: 'text', - text: `\nProgress:\n${statusMessages.join('\n')}\n\nRaw result: ${JSON.stringify(result, null, 2)}` - }); - - return { content }; + // Format the elicitation result + const content: CallToolResult["content"] = []; + + if (result.action === "accept" && result.content) { + content.push({ + type: "text", + text: `[COMPLETED] User provided the requested information!`, + }); + + const userData = result.content as Record; + const lines = []; + if (userData.name) lines.push(`- Name: ${userData.name}`); + if (userData.favoriteColor) + lines.push(`- Favorite Color: ${userData.favoriteColor}`); + if (userData.agreeToTerms !== undefined) + lines.push(`- Agreed to terms: ${userData.agreeToTerms}`); + + content.push({ + type: "text", + text: `User inputs:\n${lines.join("\n")}`, + }); + } else if (result.action === "decline") { + content.push({ + type: "text", + text: `[DECLINED] User declined to provide the requested information.`, + }); + } else if (result.action === "cancel") { + content.push({ + type: "text", + text: `[CANCELLED] User cancelled the elicitation dialog.`, + }); + } + + // Include progress and raw result for debugging + content.push({ + type: "text", + text: `\nProgress:\n${statusMessages.join( + "\n" + )}\n\nRaw result: ${JSON.stringify(result, null, 2)}`, }); - } + + return { content }; + } + ); + } }; diff --git a/src/everything/tools/trigger-elicitation-request.ts b/src/everything/tools/trigger-elicitation-request.ts index d3e3dae692..42a4bae2fe 100644 --- a/src/everything/tools/trigger-elicitation-request.ts +++ b/src/everything/tools/trigger-elicitation-request.ts @@ -1,18 +1,18 @@ -import { ElicitResultSchema } from '@modelcontextprotocol/core'; -import { McpServer, CallToolResult } from '@modelcontextprotocol/server'; +import { ElicitResultSchema } from "@modelcontextprotocol/core"; +import { McpServer, CallToolResult } from "@modelcontextprotocol/server"; // Tool configuration -const name = 'trigger-elicitation-request'; +const name = "trigger-elicitation-request"; const config = { - title: 'Trigger Elicitation Request Tool', - description: 'Trigger a Request from the Server for User Elicitation', - inputSchema: {}, - annotations: { - readOnlyHint: false, - destructiveHint: false, - idempotentHint: false, - openWorldHint: false - } + title: "Trigger Elicitation Request Tool", + description: "Trigger a Request from the Server for User Elicitation", + inputSchema: {}, + annotations: { + readOnlyHint: false, + destructiveHint: false, + idempotentHint: false, + openWorldHint: false, + }, }; /** @@ -34,178 +34,199 @@ const config = { * @param {McpServer} server - TThe McpServer instance where the tool will be registered. */ export const registerTriggerElicitationRequestTool = (server: McpServer) => { - // Does the client support elicitation? - const clientCapabilities = server.server.getClientCapabilities() || {}; - const clientSupportsElicitation: boolean = clientCapabilities.elicitation !== undefined; + // 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, ctx): Promise => { - const elicitationResult = await ctx.mcpReq.send( - { - 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: { - 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' - } - }, - required: ['name'] - } - } + // If so, register tool + if (clientSupportsElicitation) { + server.registerTool( + name, + config, + async (args, ctx): Promise => { + const elicitationResult = await ctx.mcpReq.send( + { + 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: { + 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", + }, }, - ElicitResultSchema, - { timeout: 10 * 60 * 1000 /* 10 minutes */ } - ); + required: ["name"], + }, + }, + }, + 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) { - content.push({ - type: 'text', - text: `✅ User provided the requested information!` - }); + 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}`); + // 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.` - }); - } + 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: `\nRaw result: ${JSON.stringify(elicitationResult, null, 2)}` - }); - - return { content }; + // Include raw result for debugging + content.push({ + type: "text", + text: `\nRaw result: ${JSON.stringify(elicitationResult, null, 2)}`, }); - } + + return { content }; + } + ); + } }; diff --git a/src/everything/tools/trigger-long-running-operation.ts b/src/everything/tools/trigger-long-running-operation.ts index 54f4a98324..065f3699ea 100644 --- a/src/everything/tools/trigger-long-running-operation.ts +++ b/src/everything/tools/trigger-long-running-operation.ts @@ -1,24 +1,27 @@ -import { z } from 'zod'; -import { McpServer, CallToolResult } from '@modelcontextprotocol/server'; +import { z } from "zod"; +import { McpServer, CallToolResult } from "@modelcontextprotocol/server"; // Tool input schema const TriggerLongRunningOperationSchema = z.object({ - duration: z.number().default(10).describe('Duration of the operation in seconds'), - steps: z.number().default(5).describe('Number of steps in the operation') + duration: z + .number() + .default(10) + .describe("Duration of the operation in seconds"), + steps: z.number().default(5).describe("Number of steps in the operation"), }); // Tool configuration -const name = 'trigger-long-running-operation'; +const name = "trigger-long-running-operation"; const config = { - title: 'Trigger Long Running Operation Tool', - description: 'Demonstrates a long running operation with progress updates.', - inputSchema: TriggerLongRunningOperationSchema, - annotations: { - readOnlyHint: true, - destructiveHint: false, - idempotentHint: true, - openWorldHint: false - } + title: "Trigger Long Running Operation Tool", + description: "Demonstrates a long running operation with progress updates.", + inputSchema: TriggerLongRunningOperationSchema, + annotations: { + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: false, + }, }; /** @@ -36,37 +39,43 @@ const config = { * @param {McpServer} server - The McpServer instance where the tool will be registered. */ export const registerTriggerLongRunningOperationTool = (server: McpServer) => { - server.registerTool(name, config, async (args, ctx): Promise => { - const validatedArgs = TriggerLongRunningOperationSchema.parse(args); - const { duration, steps } = validatedArgs; - const stepDuration = duration / steps; - const progressToken = ctx.mcpReq._meta?.progressToken; + server.registerTool( + name, + config, + async (args, ctx): Promise => { + const validatedArgs = TriggerLongRunningOperationSchema.parse(args); + const { duration, steps } = validatedArgs; + const stepDuration = duration / steps; + const progressToken = ctx.mcpReq._meta?.progressToken; - for (let i = 1; i < steps + 1; i++) { - await new Promise(resolve => setTimeout(resolve, stepDuration * 1000)); + for (let i = 1; i < steps + 1; i++) { + await new Promise((resolve) => + setTimeout(resolve, stepDuration * 1000) + ); - if (progressToken !== undefined) { - await server.server.notification( - { - method: 'notifications/progress', - params: { - progress: i, - total: steps, - progressToken - } - }, - { relatedRequestId: ctx.mcpReq.id } - ); - } + if (progressToken !== undefined) { + await server.server.notification( + { + method: "notifications/progress", + params: { + progress: i, + total: steps, + progressToken, + }, + }, + { relatedRequestId: ctx.mcpReq.id } + ); } + } - return { - content: [ - { - type: 'text', - text: `Long running operation completed. Duration: ${duration} seconds, Steps: ${steps}.` - } - ] - }; - }); + return { + content: [ + { + type: "text", + text: `Long running operation completed. Duration: ${duration} seconds, Steps: ${steps}.`, + }, + ], + }; + } + ); }; diff --git a/src/everything/tools/trigger-sampling-request-async.ts b/src/everything/tools/trigger-sampling-request-async.ts index 371eb7f379..3a9fa6c2c4 100644 --- a/src/everything/tools/trigger-sampling-request-async.ts +++ b/src/everything/tools/trigger-sampling-request-async.ts @@ -1,27 +1,34 @@ -import { McpServer, CallToolResult, CreateMessageRequest } from '@modelcontextprotocol/server'; -import { z } from 'zod'; +import { + McpServer, + CallToolResult, + CreateMessageRequest, +} from "@modelcontextprotocol/server"; +import { z } from "zod"; // Tool input schema const TriggerSamplingRequestAsyncSchema = z.object({ - prompt: z.string().describe('The prompt to send to the LLM'), - maxTokens: z.number().default(100).describe('Maximum number of tokens to generate') + prompt: z.string().describe("The prompt to send to the LLM"), + maxTokens: z + .number() + .default(100) + .describe("Maximum number of tokens to generate"), }); // Tool configuration -const name = 'trigger-sampling-request-async'; +const name = "trigger-sampling-request-async"; const config = { - title: 'Trigger Async Sampling Request Tool', - description: - 'Trigger an async sampling request that the CLIENT executes as a background task. ' + - 'Demonstrates bidirectional MCP tasks where the server sends a request and the client ' + - 'executes it asynchronously, allowing the server to poll for progress and results.', - inputSchema: TriggerSamplingRequestAsyncSchema, - annotations: { - readOnlyHint: false, - destructiveHint: false, - idempotentHint: false, - openWorldHint: true - } + title: "Trigger Async Sampling Request Tool", + description: + "Trigger an async sampling request that the CLIENT executes as a background task. " + + "Demonstrates bidirectional MCP tasks where the server sends a request and the client " + + "executes it asynchronously, allowing the server to poll for progress and results.", + inputSchema: TriggerSamplingRequestAsyncSchema, + annotations: { + readOnlyHint: false, + destructiveHint: false, + idempotentHint: false, + openWorldHint: true, + }, }; // Poll interval in milliseconds @@ -42,167 +49,186 @@ const MAX_POLL_ATTEMPTS = 60; * @param {McpServer} server - The McpServer instance where the tool will be registered. */ export const registerTriggerSamplingRequestAsyncTool = (server: McpServer) => { - // Check client capabilities - const clientCapabilities = server.server.getClientCapabilities() || {}; - - // Client must support sampling AND tasks.requests.sampling - const clientSupportsSampling = clientCapabilities.sampling !== undefined; - const clientTasksCapability = clientCapabilities.tasks as - | { - requests?: { sampling?: { createMessage?: object } }; - } - | undefined; - const clientSupportsAsyncSampling = clientTasksCapability?.requests?.sampling?.createMessage !== undefined; - - if (clientSupportsSampling && clientSupportsAsyncSampling) { - server.registerTool(name, config, async (args, ctx): Promise => { - const validatedArgs = TriggerSamplingRequestAsyncSchema.parse(args); - const { prompt, maxTokens } = validatedArgs; - - // Create the sampling request WITH task metadata - // The params.task field signals to the client that this should be executed as a task - const request: CreateMessageRequest & { - params: { task?: { ttl: number } }; - } = { - method: 'sampling/createMessage', - params: { - task: { - ttl: 300000 // 5 minutes - }, - messages: [ - { - role: 'user', - content: { - type: 'text', - text: `Resource ${name} context: ${prompt}` - } - } - ], - systemPrompt: 'You are a helpful test server.', - maxTokens, - temperature: 0.7 - } - }; - - // Send the sampling request - // Client may return either: - // - CreateMessageResult (synchronous execution) - // - CreateTaskResult (task-based execution with { task } object) - const samplingResponse = await ctx.mcpReq.send( - request, - z.union([ - // CreateTaskResult - client created a task - z.object({ - task: z.object({ - taskId: z.string(), - status: z.string(), - pollInterval: z.number().optional(), - statusMessage: z.string().optional() - }) - }), - // CreateMessageResult - synchronous execution - z.object({ - role: z.string(), - content: z.any(), - model: z.string(), - stopReason: z.string().optional() - }) - ]) - ); - - // Check if client returned CreateTaskResult (has task object) - const isTaskResult = 'task' in samplingResponse && samplingResponse.task; - if (!isTaskResult) { - // Client executed synchronously - return the direct response - return { - content: [ - { - type: 'text', - text: `[SYNC] Client executed synchronously:\n${JSON.stringify(samplingResponse, null, 2)}` - } - ] - }; - } - - const taskId = samplingResponse.task.taskId; - const statusMessages: string[] = []; - statusMessages.push(`Task created: ${taskId}`); - - // Poll for task completion - let attempts = 0; - let taskStatus = samplingResponse.task.status; - let taskStatusMessage: string | undefined; - - while (taskStatus !== 'completed' && taskStatus !== 'failed' && taskStatus !== 'cancelled' && attempts < MAX_POLL_ATTEMPTS) { - // Wait before polling - await new Promise(resolve => setTimeout(resolve, POLL_INTERVAL)); - attempts++; - - // Get task status from client - const pollResult = await ctx.mcpReq.send( - { - method: 'tasks/get', - params: { taskId } - }, - z.looseObject({ - status: z.string(), - statusMessage: z.string().optional() - }) - ); - - taskStatus = pollResult.status; - taskStatusMessage = pollResult.statusMessage; - statusMessages.push(`Poll ${attempts}: ${taskStatus}${taskStatusMessage ? ` - ${taskStatusMessage}` : ''}`); - } - - // Check for timeout - if (attempts >= MAX_POLL_ATTEMPTS) { - return { - content: [ - { - type: 'text', - text: `[TIMEOUT] Task timed out after ${MAX_POLL_ATTEMPTS} poll attempts\n\nProgress:\n${statusMessages.join( - '\n' - )}` - } - ] - }; - } - - // Check for failure/cancellation - if (taskStatus === 'failed' || taskStatus === 'cancelled') { - return { - content: [ - { - type: 'text', - text: `[${taskStatus.toUpperCase()}] ${taskStatusMessage || 'No message'}\n\nProgress:\n${statusMessages.join( - '\n' - )}` - } - ] - }; - } - - // Fetch the final result - const result = await ctx.mcpReq.send( - { - method: 'tasks/result', - params: { taskId } + // Check client capabilities + const clientCapabilities = server.server.getClientCapabilities() || {}; + + // Client must support sampling AND tasks.requests.sampling + const clientSupportsSampling = clientCapabilities.sampling !== undefined; + const clientTasksCapability = clientCapabilities.tasks as + | { + requests?: { sampling?: { createMessage?: object } }; + } + | undefined; + const clientSupportsAsyncSampling = + clientTasksCapability?.requests?.sampling?.createMessage !== undefined; + + if (clientSupportsSampling && clientSupportsAsyncSampling) { + server.registerTool( + name, + config, + async (args, ctx): Promise => { + const validatedArgs = TriggerSamplingRequestAsyncSchema.parse(args); + const { prompt, maxTokens } = validatedArgs; + + // Create the sampling request WITH task metadata + // The params.task field signals to the client that this should be executed as a task + const request: CreateMessageRequest & { + params: { task?: { ttl: number } }; + } = { + method: "sampling/createMessage", + params: { + task: { + ttl: 300000, // 5 minutes + }, + messages: [ + { + role: "user", + content: { + type: "text", + text: `Resource ${name} context: ${prompt}`, }, - z.any() - ); - - // Return the result with status history - return { - content: [ - { - type: 'text', - text: `[COMPLETED] Async sampling completed!\n\n**Progress:**\n${statusMessages.join( - '\n' - )}\n\n**Result:**\n${JSON.stringify(result, null, 2)}` - } - ] - }; - }); - } + }, + ], + systemPrompt: "You are a helpful test server.", + maxTokens, + temperature: 0.7, + }, + }; + + // Send the sampling request + // Client may return either: + // - CreateMessageResult (synchronous execution) + // - CreateTaskResult (task-based execution with { task } object) + const samplingResponse = await ctx.mcpReq.send( + request, + z.union([ + // CreateTaskResult - client created a task + z.object({ + task: z.object({ + taskId: z.string(), + status: z.string(), + pollInterval: z.number().optional(), + statusMessage: z.string().optional(), + }), + }), + // CreateMessageResult - synchronous execution + z.object({ + role: z.string(), + content: z.any(), + model: z.string(), + stopReason: z.string().optional(), + }), + ]) + ); + + // Check if client returned CreateTaskResult (has task object) + const isTaskResult = + "task" in samplingResponse && samplingResponse.task; + if (!isTaskResult) { + // Client executed synchronously - return the direct response + return { + content: [ + { + type: "text", + text: `[SYNC] Client executed synchronously:\n${JSON.stringify( + samplingResponse, + null, + 2 + )}`, + }, + ], + }; + } + + const taskId = samplingResponse.task.taskId; + const statusMessages: string[] = []; + statusMessages.push(`Task created: ${taskId}`); + + // Poll for task completion + let attempts = 0; + let taskStatus = samplingResponse.task.status; + let taskStatusMessage: string | undefined; + + while ( + taskStatus !== "completed" && + taskStatus !== "failed" && + taskStatus !== "cancelled" && + attempts < MAX_POLL_ATTEMPTS + ) { + // Wait before polling + await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL)); + attempts++; + + // Get task status from client + const pollResult = await ctx.mcpReq.send( + { + method: "tasks/get", + params: { taskId }, + }, + z.looseObject({ + status: z.string(), + statusMessage: z.string().optional(), + }) + ); + + taskStatus = pollResult.status; + taskStatusMessage = pollResult.statusMessage; + statusMessages.push( + `Poll ${attempts}: ${taskStatus}${ + taskStatusMessage ? ` - ${taskStatusMessage}` : "" + }` + ); + } + + // Check for timeout + if (attempts >= MAX_POLL_ATTEMPTS) { + return { + content: [ + { + type: "text", + text: `[TIMEOUT] Task timed out after ${MAX_POLL_ATTEMPTS} poll attempts\n\nProgress:\n${statusMessages.join( + "\n" + )}`, + }, + ], + }; + } + + // Check for failure/cancellation + if (taskStatus === "failed" || taskStatus === "cancelled") { + return { + content: [ + { + type: "text", + text: `[${taskStatus.toUpperCase()}] ${ + taskStatusMessage || "No message" + }\n\nProgress:\n${statusMessages.join("\n")}`, + }, + ], + }; + } + + // Fetch the final result + const result = await ctx.mcpReq.send( + { + method: "tasks/result", + params: { taskId }, + }, + z.any() + ); + + // Return the result with status history + return { + content: [ + { + type: "text", + text: `[COMPLETED] Async sampling completed!\n\n**Progress:**\n${statusMessages.join( + "\n" + )}\n\n**Result:**\n${JSON.stringify(result, null, 2)}`, + }, + ], + }; + } + ); + } }; diff --git a/src/everything/tools/trigger-sampling-request.ts b/src/everything/tools/trigger-sampling-request.ts index 54ab0f6a07..ede09182ed 100644 --- a/src/everything/tools/trigger-sampling-request.ts +++ b/src/everything/tools/trigger-sampling-request.ts @@ -1,25 +1,32 @@ -import { CreateMessageResultSchema } from '@modelcontextprotocol/core'; -import { McpServer, CallToolResult, CreateMessageRequest } from '@modelcontextprotocol/server'; -import { z } from 'zod'; +import { CreateMessageResultSchema } from "@modelcontextprotocol/core"; +import { + McpServer, + CallToolResult, + CreateMessageRequest, +} from "@modelcontextprotocol/server"; +import { z } from "zod"; // Tool input schema const TriggerSamplingRequestSchema = z.object({ - prompt: z.string().describe('The prompt to send to the LLM'), - maxTokens: z.number().default(100).describe('Maximum number of tokens to generate') + prompt: z.string().describe("The prompt to send to the LLM"), + maxTokens: z + .number() + .default(100) + .describe("Maximum number of tokens to generate"), }); // Tool configuration -const name = 'trigger-sampling-request'; +const name = "trigger-sampling-request"; const config = { - title: 'Trigger Sampling Request Tool', - description: 'Trigger a Request from the Server for LLM Sampling', - inputSchema: TriggerSamplingRequestSchema, - annotations: { - readOnlyHint: false, - destructiveHint: false, - idempotentHint: false, - openWorldHint: true - } + title: "Trigger Sampling Request Tool", + description: "Trigger a Request from the Server for LLM Sampling", + inputSchema: TriggerSamplingRequestSchema, + annotations: { + readOnlyHint: false, + destructiveHint: false, + idempotentHint: false, + openWorldHint: true, + }, }; /** @@ -36,47 +43,55 @@ const config = { * @param {McpServer} server - The McpServer instance where the tool will be registered. */ export const registerTriggerSamplingRequestTool = (server: McpServer) => { - // Does the client support sampling? - const clientCapabilities = server.server.getClientCapabilities() || {}; - const clientSupportsSampling: boolean = clientCapabilities.sampling !== undefined; + // Does the client support sampling? + const clientCapabilities = server.server.getClientCapabilities() || {}; + const clientSupportsSampling: boolean = + clientCapabilities.sampling !== undefined; - // If so, register tool - if (clientSupportsSampling) { - server.registerTool(name, config, async (args, ctx): Promise => { - const validatedArgs = TriggerSamplingRequestSchema.parse(args); - const { prompt, maxTokens } = validatedArgs; + // If so, register tool + if (clientSupportsSampling) { + server.registerTool( + name, + config, + async (args, ctx): Promise => { + const validatedArgs = TriggerSamplingRequestSchema.parse(args); + const { prompt, maxTokens } = validatedArgs; - // Create the sampling request - const request: CreateMessageRequest = { - method: 'sampling/createMessage', - params: { - messages: [ - { - role: 'user', - content: { - type: 'text', - text: `Resource ${name} context: ${prompt}` - } - } - ], - systemPrompt: 'You are a helpful test server.', - maxTokens, - temperature: 0.7 - } - }; + // Create the sampling request + const request: CreateMessageRequest = { + method: "sampling/createMessage", + params: { + messages: [ + { + role: "user", + content: { + type: "text", + text: `Resource ${name} context: ${prompt}`, + }, + }, + ], + systemPrompt: "You are a helpful test server.", + maxTokens, + temperature: 0.7, + }, + }; - // Send the sampling request to the client - const result = await ctx.mcpReq.send(request, CreateMessageResultSchema); + // Send the sampling request to the client + const result = await ctx.mcpReq.send( + request, + CreateMessageResultSchema + ); - // Return the result to the client - return { - content: [ - { - type: 'text', - text: `LLM sampling result: \n${JSON.stringify(result, null, 2)}` - } - ] - }; - }); - } + // Return the result to the client + return { + content: [ + { + type: "text", + text: `LLM sampling result: \n${JSON.stringify(result, null, 2)}`, + }, + ], + }; + } + ); + } }; diff --git a/src/everything/tools/trigger-url-elicitation.ts b/src/everything/tools/trigger-url-elicitation.ts index 91e1015d34..15e4851e3f 100644 --- a/src/everything/tools/trigger-url-elicitation.ts +++ b/src/everything/tools/trigger-url-elicitation.ts @@ -1,45 +1,53 @@ -import { randomUUID } from 'node:crypto'; -import { ElicitResultSchema } from '@modelcontextprotocol/core'; -import { McpServer, CallToolResult, ElicitRequestURLParams, UrlElicitationRequiredError } from '@modelcontextprotocol/server'; -import { z } from 'zod'; +import { randomUUID } from "node:crypto"; +import { ElicitResultSchema } from "@modelcontextprotocol/core"; +import { + McpServer, + CallToolResult, + ElicitRequestURLParams, + UrlElicitationRequiredError, +} from "@modelcontextprotocol/server"; +import { z } from "zod"; // Tool input schema const TriggerUrlElicitationSchema = z.object({ - url: z.string().url().describe('The URL the user should open'), - message: z - .string() - .default('Please open the link to complete this action.') - .describe('Message shown to the user before opening the URL'), - elicitationId: z.string().optional().describe('Optional explicit elicitation ID. Defaults to a random UUID.'), - errorPath: z - .boolean() - .default(false) - .describe( - 'Controls which elicitation mechanism is used. ' + - 'When false (default), sends an elicitation/create request (request path). ' + - 'When true, throws a UrlElicitationRequiredError (MCP error code -32042) so the client handles ' + - 'the URL elicitation via the error path rather than waiting for a response. ' + - 'To clear the error, satisfy the prerequisite and retry this call with the same arguments; the ' + - 'retry ignores errorPath and proceeds, so the client does not loop on the same error.' - ) + url: z.string().url().describe("The URL the user should open"), + message: z + .string() + .default("Please open the link to complete this action.") + .describe("Message shown to the user before opening the URL"), + elicitationId: z + .string() + .optional() + .describe("Optional explicit elicitation ID. Defaults to a random UUID."), + errorPath: z + .boolean() + .default(false) + .describe( + "Controls which elicitation mechanism is used. " + + "When false (default), sends an elicitation/create request (request path). " + + "When true, throws a UrlElicitationRequiredError (MCP error code -32042) so the client handles " + + "the URL elicitation via the error path rather than waiting for a response. " + + "To clear the error, satisfy the prerequisite and retry this call with the same arguments; the " + + "retry ignores errorPath and proceeds, so the client does not loop on the same error." + ), }); // Tool configuration -const name = 'trigger-url-elicitation'; +const name = "trigger-url-elicitation"; const config = { - title: 'Trigger URL Elicitation Tool', - description: - 'Trigger a URL elicitation so the client can direct the user to a browser flow. ' + - "Supports two mechanisms: the request path (elicitation/create, default) which awaits the user's " + - 'response, and the error path (UrlElicitationRequiredError, -32042) which signals the client ' + - 'to handle URL elicitation via the error response. Set errorPath=true to use the error path.', - inputSchema: TriggerUrlElicitationSchema, - annotations: { - readOnlyHint: false, - destructiveHint: false, - idempotentHint: false, - openWorldHint: true - } + title: "Trigger URL Elicitation Tool", + description: + "Trigger a URL elicitation so the client can direct the user to a browser flow. " + + "Supports two mechanisms: the request path (elicitation/create, default) which awaits the user's " + + "response, and the error path (UrlElicitationRequiredError, -32042) which signals the client " + + "to handle URL elicitation via the error response. Set errorPath=true to use the error path.", + inputSchema: TriggerUrlElicitationSchema, + annotations: { + readOnlyHint: false, + destructiveHint: false, + idempotentHint: false, + openWorldHint: true, + }, }; /** @@ -65,7 +73,8 @@ const issuedErrorPathElicitations = new Set(); * Test-only helper to reset the module-level error-path state between cases. * Not part of the tool's public behavior. */ -export const __resetIssuedErrorPathElicitations = () => issuedErrorPathElicitations.clear(); +export const __resetIssuedErrorPathElicitations = () => + issuedErrorPathElicitations.clear(); /** * Registers the 'trigger-url-elicitation' tool. @@ -84,106 +93,125 @@ export const __resetIssuedErrorPathElicitations = () => issuedErrorPathElicitati * @param {McpServer} server - The McpServer instance where the tool will be registered. */ export const registerTriggerUrlElicitationTool = (server: McpServer) => { - const clientCapabilities = server.server.getClientCapabilities() || {}; - const clientElicitationCapabilities = clientCapabilities.elicitation as - | { - url?: object; - } - | undefined; - - const clientSupportsUrlElicitation = clientElicitationCapabilities?.url !== undefined; - - if (clientSupportsUrlElicitation) { - server.registerTool(name, config, async (args, ctx): Promise => { - const { url, message, elicitationId: requestedElicitationId, errorPath } = args; - - const elicitationId = requestedElicitationId ?? randomUUID(); - const sessionId = ctx.sessionId ?? 'default'; - - // Key the one-shot error-path marker on inputs the client resends - // verbatim when it retries the original tool call. A real client retries - // with the *same* arguments and does NOT echo the prerequisite's - // (server-generated) elicitationId, so we must key on stable inputs: - // the session, the requested URL, and the caller-supplied elicitationId - // (if any). Keying on the resolved/random elicitationId would change on - // every call and never match, re-throwing the prerequisite forever. - const errorPathKey = `${sessionId}\u0000${url}\u0000${requestedElicitationId ?? ''}`; - - const elicitationParams: ElicitRequestURLParams = { - mode: 'url', - url, - message, - elicitationId + const clientCapabilities = server.server.getClientCapabilities() || {}; + const clientElicitationCapabilities = clientCapabilities.elicitation as + | { + url?: object; + } + | undefined; + + const clientSupportsUrlElicitation = + clientElicitationCapabilities?.url !== undefined; + + if (clientSupportsUrlElicitation) { + server.registerTool( + name, + config, + async (args, ctx): Promise => { + const { + url, + message, + elicitationId: requestedElicitationId, + errorPath, + } = args; + + const elicitationId = requestedElicitationId ?? randomUUID(); + const sessionId = ctx.sessionId ?? "default"; + + // Key the one-shot error-path marker on inputs the client resends + // verbatim when it retries the original tool call. A real client retries + // with the *same* arguments and does NOT echo the prerequisite's + // (server-generated) elicitationId, so we must key on stable inputs: + // the session, the requested URL, and the caller-supplied elicitationId + // (if any). Keying on the resolved/random elicitationId would change on + // every call and never match, re-throwing the prerequisite forever. + const errorPathKey = `${sessionId}\u0000${url}\u0000${ + requestedElicitationId ?? "" + }`; + + const elicitationParams: ElicitRequestURLParams = { + mode: "url", + url, + message, + elicitationId, + }; + + // Error path: signal the client via UrlElicitationRequiredError (-32042) + // so it handles a prerequisite URL elicitation before this request can + // proceed. Two things keep the client from looping forever: + // + // 1. The prerequisite points at a *different* URL than the one that + // failed. Reusing the original `url` would make the client complete + // the prerequisite, retry, and hit the same -32042 error endlessly. + // 2. We remember that we issued a prerequisite for this request. When + // the client satisfies it and retries the same call, we recognize + // the retry, *ignore* errorPath, and fall through to the request + // path. Without this, the retry would re-enter the error path and + // re-request the prerequisite URL — another loop. + if (errorPath) { + if (issuedErrorPathElicitations.has(errorPathKey)) { + // Retry of a satisfied prerequisite: clear the one-shot marker and + // ignore errorPath, falling through to the request path below. + issuedErrorPathElicitations.delete(errorPathKey); + } else { + // Originating call: record that we issued a prerequisite for this + // request, then signal the client via -32042. + issuedErrorPathElicitations.add(errorPathKey); + const prerequisiteElicitation: ElicitRequestURLParams = { + mode: "url", + url: "https://modelcontextprotocol.io", + message: + "Open this link to satisfy the prerequisite, then retry the request.", + elicitationId: randomUUID(), }; - - // Error path: signal the client via UrlElicitationRequiredError (-32042) - // so it handles a prerequisite URL elicitation before this request can - // proceed. Two things keep the client from looping forever: - // - // 1. The prerequisite points at a *different* URL than the one that - // failed. Reusing the original `url` would make the client complete - // the prerequisite, retry, and hit the same -32042 error endlessly. - // 2. We remember that we issued a prerequisite for this request. When - // the client satisfies it and retries the same call, we recognize - // the retry, *ignore* errorPath, and fall through to the request - // path. Without this, the retry would re-enter the error path and - // re-request the prerequisite URL — another loop. - if (errorPath) { - if (issuedErrorPathElicitations.has(errorPathKey)) { - // Retry of a satisfied prerequisite: clear the one-shot marker and - // ignore errorPath, falling through to the request path below. - issuedErrorPathElicitations.delete(errorPathKey); - } else { - // Originating call: record that we issued a prerequisite for this - // request, then signal the client via -32042. - issuedErrorPathElicitations.add(errorPathKey); - const prerequisiteElicitation: ElicitRequestURLParams = { - mode: 'url', - url: 'https://modelcontextprotocol.io', - message: 'Open this link to satisfy the prerequisite, then retry the request.', - elicitationId: randomUUID() - }; - throw new UrlElicitationRequiredError([prerequisiteElicitation], 'This request requires browser-based authorization.'); - } - } - - // Request path: send elicitation/create and await the user's response - const elicitationResult = await ctx.mcpReq.send( - { - method: 'elicitation/create', - params: elicitationParams - }, - ElicitResultSchema, - { timeout: 10 * 60 * 1000 /* 10 minutes */ } + throw new UrlElicitationRequiredError( + [prerequisiteElicitation], + "This request requires browser-based authorization." ); - - // Handle different response actions - const content: CallToolResult['content'] = []; - - if (elicitationResult.action === 'accept') { - content.push({ - type: 'text', - text: `✅ User completed the URL elicitation flow.\n` + `Elicitation ID: ${elicitationId}\n` + `URL: ${url}` - }); - } else if (elicitationResult.action === 'decline') { - content.push({ - type: 'text', - text: `❌ User declined to open the URL (Elicitation ID: ${elicitationId}).` - }); - } else if (elicitationResult.action === 'cancel') { - content.push({ - type: 'text', - text: `⚠️ User cancelled the URL elicitation (Elicitation ID: ${elicitationId}).` - }); - } - - // Include raw result for debugging - content.push({ - type: 'text', - text: `\nRaw result: ${JSON.stringify(elicitationResult, null, 2)}` - }); - - return { content }; + } + } + + // Request path: send elicitation/create and await the user's response + const elicitationResult = await ctx.mcpReq.send( + { + method: "elicitation/create", + params: elicitationParams, + }, + ElicitResultSchema, + { timeout: 10 * 60 * 1000 /* 10 minutes */ } + ); + + // Handle different response actions + const content: CallToolResult["content"] = []; + + if (elicitationResult.action === "accept") { + content.push({ + type: "text", + text: + `✅ User completed the URL elicitation flow.\n` + + `Elicitation ID: ${elicitationId}\n` + + `URL: ${url}`, + }); + } else if (elicitationResult.action === "decline") { + content.push({ + type: "text", + text: `❌ User declined to open the URL (Elicitation ID: ${elicitationId}).`, + }); + } else if (elicitationResult.action === "cancel") { + content.push({ + type: "text", + text: `⚠️ User cancelled the URL elicitation (Elicitation ID: ${elicitationId}).`, + }); + } + + // Include raw result for debugging + content.push({ + type: "text", + text: `\nRaw result: ${JSON.stringify(elicitationResult, null, 2)}`, }); - } + + return { content }; + } + ); + } }; diff --git a/src/everything/transports/sse.ts b/src/everything/transports/sse.ts index 1c4f096b9e..3283325fcb 100644 --- a/src/everything/transports/sse.ts +++ b/src/everything/transports/sse.ts @@ -1,74 +1,77 @@ -import { SSEServerTransport } from '@modelcontextprotocol/server-legacy/sse'; -import express from 'express'; -import { createServer } from '../server/index.js'; -import cors from 'cors'; +import { SSEServerTransport } from "@modelcontextprotocol/server-legacy/sse"; +import express from "express"; +import { createServer } from "../server/index.js"; +import cors from "cors"; -console.error('Starting SSE server...'); +console.error("Starting SSE server..."); // Express app with permissive CORS for testing with Inspector direct connect mode const app = express(); app.use( - cors({ - origin: '*', // use "*" with caution in production - methods: 'GET,POST', - preflightContinue: false, - optionsSuccessStatus: 204 - }) + cors({ + origin: "*", // use "*" with caution in production + methods: "GET,POST", + preflightContinue: false, + optionsSuccessStatus: 204, + }) ); // Map sessionId to transport for each client -const transports: Map = new Map(); +const transports: Map = new Map< + string, + SSEServerTransport +>(); // Handle GET requests for new SSE streams -app.get('/sse', async (req, res) => { - let transport: SSEServerTransport; - const { server, cleanup } = createServer(); +app.get("/sse", async (req, res) => { + let transport: SSEServerTransport; + const { server, cleanup } = createServer(); - // Session Id should not exist for GET /sse requests - if (req?.query?.sessionId) { - const sessionId = req?.query?.sessionId as string; - transport = transports.get(sessionId) as SSEServerTransport; - console.error( - "Client Reconnecting? This shouldn't happen; when client has a sessionId, GET /sse should not be called again.", - transport.sessionId - ); - } else { - // Create and store transport for the new session - transport = new SSEServerTransport('/message', res); - transports.set(transport.sessionId, transport); + // Session Id should not exist for GET /sse requests + if (req?.query?.sessionId) { + const sessionId = req?.query?.sessionId as string; + transport = transports.get(sessionId) as SSEServerTransport; + console.error( + "Client Reconnecting? This shouldn't happen; when client has a sessionId, GET /sse should not be called again.", + transport.sessionId + ); + } else { + // Create and store transport for the new session + transport = new SSEServerTransport("/message", res); + transports.set(transport.sessionId, transport); - // Connect server to transport - await server.connect(transport); - const sessionId = transport.sessionId; - console.error('Client Connected: ', sessionId); + // Connect server to transport + await server.connect(transport); + const sessionId = transport.sessionId; + console.error("Client Connected: ", sessionId); - // Handle close of connection - server.server.onclose = async () => { - const sessionId = transport.sessionId; - console.error('Client Disconnected: ', sessionId); - transports.delete(sessionId); - cleanup(sessionId); - }; - } + // Handle close of connection + server.server.onclose = async () => { + const sessionId = transport.sessionId; + console.error("Client Disconnected: ", sessionId); + transports.delete(sessionId); + cleanup(sessionId); + }; + } }); // Handle POST requests for client messages -app.post('/message', async (req, res) => { - // Session Id should exist for POST /message requests - const sessionId = req?.query?.sessionId as string; +app.post("/message", async (req, res) => { + // Session Id should exist for POST /message requests + const sessionId = req?.query?.sessionId as string; - // Get the transport for this session and use it to handle the request - const transport = transports.get(sessionId); - if (transport) { - console.error('Client Message from', sessionId); - await transport.handlePostMessage(req, res); - } else { - console.error(`No transport found for sessionId ${sessionId}`); - } + // Get the transport for this session and use it to handle the request + const transport = transports.get(sessionId); + if (transport) { + console.error("Client Message from", sessionId); + await transport.handlePostMessage(req, res); + } else { + console.error(`No transport found for sessionId ${sessionId}`); + } }); // Start the express server const PORT = process.env.PORT || 3001; app.listen(PORT, () => { - console.error(`Server is running on port ${PORT}`); + console.error(`Server is running on port ${PORT}`); }); diff --git a/src/everything/transports/stdio.ts b/src/everything/transports/stdio.ts index a72d3bad2d..111d79d8a4 100644 --- a/src/everything/transports/stdio.ts +++ b/src/everything/transports/stdio.ts @@ -1,9 +1,9 @@ #!/usr/bin/env node -import { StdioServerTransport } from '@modelcontextprotocol/server/stdio'; -import { createServer } from '../server/index.js'; +import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; +import { createServer } from "../server/index.js"; -console.error('Starting default (STDIO) server...'); +console.error("Starting default (STDIO) server..."); /** * The main method @@ -13,21 +13,21 @@ console.error('Starting default (STDIO) server...'); * @return {Promise} A promise that resolves when the main function has executed and the process exits. */ async function main(): Promise { - const transport = new StdioServerTransport(); - const { server, cleanup } = createServer(); + const transport = new StdioServerTransport(); + const { server, cleanup } = createServer(); - // Connect transport to server - await server.connect(transport); + // Connect transport to server + await server.connect(transport); - // Cleanup on exit - process.on('SIGINT', async () => { - await server.close(); - cleanup(); - process.exit(0); - }); + // Cleanup on exit + process.on("SIGINT", async () => { + await server.close(); + cleanup(); + process.exit(0); + }); } -main().catch(error => { - console.error('Server error:', error); - process.exit(1); +main().catch((error) => { + console.error("Server error:", error); + process.exit(1); }); diff --git a/src/everything/transports/streamableHttp.ts b/src/everything/transports/streamableHttp.ts index 251d27e9e3..70d4ee8b06 100644 --- a/src/everything/transports/streamableHttp.ts +++ b/src/everything/transports/streamableHttp.ts @@ -1,227 +1,238 @@ -import { EventStore } from '@modelcontextprotocol/server'; -import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node'; -import express, { Request, Response } from 'express'; -import { createServer } from '../server/index.js'; -import { randomUUID } from 'node:crypto'; -import cors from 'cors'; +import { EventStore } from "@modelcontextprotocol/server"; +import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node"; +import express, { Request, Response } from "express"; +import { createServer } from "../server/index.js"; +import { randomUUID } from "node:crypto"; +import cors from "cors"; // Simple in-memory event store for SSE resumability class InMemoryEventStore implements EventStore { - private events: Map = new Map(); - - async storeEvent(streamId: string, message: unknown): Promise { - const eventId = randomUUID(); - this.events.set(eventId, { streamId, message }); - return eventId; - } - - async replayEventsAfter( - lastEventId: string, - { send }: { send: (eventId: string, message: unknown) => Promise } - ): Promise { - const entries = Array.from(this.events.entries()); - const startIndex = entries.findIndex(([id]) => id === lastEventId); - if (startIndex === -1) return lastEventId; - - let lastId: string = lastEventId; - for (let i = startIndex + 1; i < entries.length; i++) { - const [eventId, { message }] = entries[i]; - await send(eventId, message); - lastId = eventId; - } - return lastId; + private events: Map = + new Map(); + + async storeEvent(streamId: string, message: unknown): Promise { + const eventId = randomUUID(); + this.events.set(eventId, { streamId, message }); + return eventId; + } + + async replayEventsAfter( + lastEventId: string, + { send }: { send: (eventId: string, message: unknown) => Promise } + ): Promise { + const entries = Array.from(this.events.entries()); + const startIndex = entries.findIndex(([id]) => id === lastEventId); + if (startIndex === -1) return lastEventId; + + let lastId: string = lastEventId; + for (let i = startIndex + 1; i < entries.length; i++) { + const [eventId, { message }] = entries[i]; + await send(eventId, message); + lastId = eventId; } + return lastId; + } } -console.log('Starting Streamable HTTP server...'); +console.log("Starting Streamable HTTP server..."); // Express app with permissive CORS for testing with Inspector direct connect mode const app = express(); app.use( - cors({ - origin: '*', // use "*" with caution in production - methods: 'GET,POST,DELETE', - preflightContinue: false, - optionsSuccessStatus: 204, - exposedHeaders: ['mcp-session-id', 'last-event-id', 'mcp-protocol-version'] - }) + cors({ + origin: "*", // use "*" with caution in production + methods: "GET,POST,DELETE", + preflightContinue: false, + optionsSuccessStatus: 204, + exposedHeaders: ["mcp-session-id", "last-event-id", "mcp-protocol-version"], + }) ); // Map sessionId to server transport for each client -const transports: Map = new Map(); +const transports: Map = new Map< + string, + NodeStreamableHTTPServerTransport +>(); // Handle POST requests for client messages -app.post('/mcp', async (req: Request, res: Response) => { - console.log('Received MCP POST request'); - try { - // Check for existing session ID - const sessionId = req.headers['mcp-session-id'] as string | undefined; - - let transport: NodeStreamableHTTPServerTransport; - - if (sessionId && transports.has(sessionId)) { - // Reuse existing transport - transport = transports.get(sessionId)!; - } else if (!sessionId) { - const { server, cleanup } = createServer(); - - // New initialization request - const eventStore = new InMemoryEventStore(); - transport = new NodeStreamableHTTPServerTransport({ - sessionIdGenerator: () => randomUUID(), - eventStore, // Enable resumability - onsessioninitialized: (sessionId: string) => { - // Store the transport by session ID when a session is initialized - // This avoids race conditions where requests might come in before the session is stored - console.log(`Session initialized with ID: ${sessionId}`); - transports.set(sessionId, transport); - } - }); - - // Set up onclose handler to clean up transport when closed - server.server.onclose = async () => { - const sid = transport.sessionId; - if (sid && transports.has(sid)) { - console.log(`Transport closed for session ${sid}, removing from transports map`); - transports.delete(sid); - cleanup(sid); - } - }; - - // Connect the transport to the MCP server BEFORE handling the request - // so responses can flow back through the same transport - await server.connect(transport); - await transport.handleRequest(req, res); - return; - } else { - // Invalid request - no session ID or not initialization request - res.status(400).json({ - jsonrpc: '2.0', - error: { - code: -32000, - message: 'Bad Request: No valid session ID provided' - }, - id: req?.body?.id - }); - return; - } - - // Handle the request with existing transport - no need to reconnect - // The existing transport is already connected to the server - await transport.handleRequest(req, res); - } catch (error) { - console.log('Error handling MCP request:', error); - if (!res.headersSent) { - res.status(500).json({ - jsonrpc: '2.0', - error: { - code: -32603, - message: 'Internal server error' - }, - id: req?.body?.id - }); - return; +app.post("/mcp", async (req: Request, res: Response) => { + console.log("Received MCP POST request"); + try { + // Check for existing session ID + const sessionId = req.headers["mcp-session-id"] as string | undefined; + + let transport: NodeStreamableHTTPServerTransport; + + if (sessionId && transports.has(sessionId)) { + // Reuse existing transport + transport = transports.get(sessionId)!; + } else if (!sessionId) { + const { server, cleanup } = createServer(); + + // New initialization request + const eventStore = new InMemoryEventStore(); + transport = new NodeStreamableHTTPServerTransport({ + sessionIdGenerator: () => randomUUID(), + eventStore, // Enable resumability + onsessioninitialized: (sessionId: string) => { + // Store the transport by session ID when a session is initialized + // This avoids race conditions where requests might come in before the session is stored + console.log(`Session initialized with ID: ${sessionId}`); + transports.set(sessionId, transport); + }, + }); + + // Set up onclose handler to clean up transport when closed + server.server.onclose = async () => { + const sid = transport.sessionId; + if (sid && transports.has(sid)) { + console.log( + `Transport closed for session ${sid}, removing from transports map` + ); + transports.delete(sid); + cleanup(sid); } - } -}); + }; -// Handle GET requests for SSE streams -app.get('/mcp', async (req: Request, res: Response) => { - console.log('Received MCP GET request'); - const sessionId = req.headers['mcp-session-id'] as string | undefined; - if (!sessionId || !transports.has(sessionId)) { - res.status(400).json({ - jsonrpc: '2.0', - error: { - code: -32000, - message: 'Bad Request: No valid session ID provided' - }, - id: req?.body?.id - }); - return; + // Connect the transport to the MCP server BEFORE handling the request + // so responses can flow back through the same transport + await server.connect(transport); + await transport.handleRequest(req, res); + return; + } else { + // Invalid request - no session ID or not initialization request + res.status(400).json({ + jsonrpc: "2.0", + error: { + code: -32000, + message: "Bad Request: No valid session ID provided", + }, + id: req?.body?.id, + }); + return; } - // Check for Last-Event-ID header for resumability - const lastEventId = req.headers['last-event-id'] as string | undefined; - if (lastEventId) { - console.log(`Client reconnecting with Last-Event-ID: ${lastEventId}`); - } else { - console.log(`Establishing new SSE stream for session ${sessionId}`); + // Handle the request with existing transport - no need to reconnect + // The existing transport is already connected to the server + await transport.handleRequest(req, res); + } catch (error) { + console.log("Error handling MCP request:", error); + if (!res.headersSent) { + res.status(500).json({ + jsonrpc: "2.0", + error: { + code: -32603, + message: "Internal server error", + }, + id: req?.body?.id, + }); + return; } + } +}); - const transport = transports.get(sessionId); - await transport!.handleRequest(req, res); +// Handle GET requests for SSE streams +app.get("/mcp", async (req: Request, res: Response) => { + console.log("Received MCP GET request"); + const sessionId = req.headers["mcp-session-id"] as string | undefined; + if (!sessionId || !transports.has(sessionId)) { + res.status(400).json({ + jsonrpc: "2.0", + error: { + code: -32000, + message: "Bad Request: No valid session ID provided", + }, + id: req?.body?.id, + }); + return; + } + + // Check for Last-Event-ID header for resumability + const lastEventId = req.headers["last-event-id"] as string | undefined; + if (lastEventId) { + console.log(`Client reconnecting with Last-Event-ID: ${lastEventId}`); + } else { + console.log(`Establishing new SSE stream for session ${sessionId}`); + } + + const transport = transports.get(sessionId); + await transport!.handleRequest(req, res); }); // Handle DELETE requests for session termination -app.delete('/mcp', async (req: Request, res: Response) => { - const sessionId = req.headers['mcp-session-id'] as string | undefined; - if (!sessionId || !transports.has(sessionId)) { - res.status(400).json({ - jsonrpc: '2.0', - error: { - code: -32000, - message: 'Bad Request: No valid session ID provided' - }, - id: req?.body?.id - }); - return; - } - - console.log(`Received session termination request for session ${sessionId}`); - - try { - const transport = transports.get(sessionId); - await transport!.handleRequest(req, res); - } catch (error) { - console.log('Error handling session termination:', error); - if (!res.headersSent) { - res.status(500).json({ - jsonrpc: '2.0', - error: { - code: -32603, - message: 'Error handling session termination' - }, - id: req?.body?.id - }); - return; - } +app.delete("/mcp", async (req: Request, res: Response) => { + const sessionId = req.headers["mcp-session-id"] as string | undefined; + if (!sessionId || !transports.has(sessionId)) { + res.status(400).json({ + jsonrpc: "2.0", + error: { + code: -32000, + message: "Bad Request: No valid session ID provided", + }, + id: req?.body?.id, + }); + return; + } + + console.log(`Received session termination request for session ${sessionId}`); + + try { + const transport = transports.get(sessionId); + await transport!.handleRequest(req, res); + } catch (error) { + console.log("Error handling session termination:", error); + if (!res.headersSent) { + res.status(500).json({ + jsonrpc: "2.0", + error: { + code: -32603, + message: "Error handling session termination", + }, + id: req?.body?.id, + }); + return; } + } }); // Start the server const PORT = process.env.PORT || 3001; const server = app.listen(PORT, () => { - console.error(`MCP Streamable HTTP Server listening on port ${PORT}`); + console.error(`MCP Streamable HTTP Server listening on port ${PORT}`); }); // Handle server errors -server.on('error', (err: unknown) => { - const code = typeof err === 'object' && err !== null && 'code' in err ? (err as { code?: unknown }).code : undefined; - if (code === 'EADDRINUSE') { - console.error(`Failed to start: Port ${PORT} is already in use. Set PORT to a free port or stop the conflicting process.`); - } else { - console.error('HTTP server encountered an error while starting:', err); - } - // Ensure a non-zero exit so npm reports the failure instead of silently exiting - process.exit(1); +server.on("error", (err: unknown) => { + const code = + typeof err === "object" && err !== null && "code" in err + ? (err as { code?: unknown }).code + : undefined; + if (code === "EADDRINUSE") { + console.error( + `Failed to start: Port ${PORT} is already in use. Set PORT to a free port or stop the conflicting process.` + ); + } else { + console.error("HTTP server encountered an error while starting:", err); + } + // Ensure a non-zero exit so npm reports the failure instead of silently exiting + process.exit(1); }); // Handle server shutdown -process.on('SIGINT', async () => { - console.log('Shutting down server...'); - - // Close all active transports to properly clean up resources - for (const sessionId in transports) { - try { - console.log(`Closing transport for session ${sessionId}`); - await transports.get(sessionId)!.close(); - transports.delete(sessionId); - } catch (error) { - console.log(`Error closing transport for session ${sessionId}:`, error); - } +process.on("SIGINT", async () => { + console.log("Shutting down server..."); + + // Close all active transports to properly clean up resources + for (const sessionId in transports) { + try { + console.log(`Closing transport for session ${sessionId}`); + await transports.get(sessionId)!.close(); + transports.delete(sessionId); + } catch (error) { + console.log(`Error closing transport for session ${sessionId}:`, error); } + } - console.log('Server shutdown complete'); - process.exit(0); + console.log("Server shutdown complete"); + process.exit(0); }); diff --git a/src/everything/tsconfig.json b/src/everything/tsconfig.json index bc21186337..829d52d66b 100644 --- a/src/everything/tsconfig.json +++ b/src/everything/tsconfig.json @@ -1,8 +1,8 @@ { - "extends": "../../tsconfig.json", - "compilerOptions": { - "outDir": "./dist", - "rootDir": "." - }, - "include": ["./**/*.ts"] + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "." + }, + "include": ["./**/*.ts"] } diff --git a/src/everything/vitest.config.ts b/src/everything/vitest.config.ts index f5889cbedf..e3149b5492 100644 --- a/src/everything/vitest.config.ts +++ b/src/everything/vitest.config.ts @@ -1,14 +1,14 @@ -import { defineConfig } from 'vitest/config'; +import { defineConfig } from "vitest/config"; export default defineConfig({ - test: { - globals: true, - environment: 'node', - include: ['**/__tests__/**/*.test.ts'], - coverage: { - provider: 'v8', - include: ['**/*.ts'], - exclude: ['**/__tests__/**', '**/dist/**'] - } - } + test: { + globals: true, + environment: "node", + include: ["**/__tests__/**/*.test.ts"], + coverage: { + provider: "v8", + include: ["**/*.ts"], + exclude: ["**/__tests__/**", "**/dist/**"], + }, + }, }); From f400370778c3b931b1c553b546b5c6a3681489c0 Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Wed, 1 Jul 2026 13:11:50 +0300 Subject: [PATCH 3/4] memory migration --- src/memory/__tests__/resource.test.ts | 17 +++--- src/memory/index.ts | 79 +++++++++++++-------------- src/memory/package.json | 3 +- 3 files changed, 49 insertions(+), 50 deletions(-) diff --git a/src/memory/__tests__/resource.test.ts b/src/memory/__tests__/resource.test.ts index 432633050b..ed1f132962 100644 --- a/src/memory/__tests__/resource.test.ts +++ b/src/memory/__tests__/resource.test.ts @@ -1,6 +1,5 @@ import { describe, it, expect, vi } from 'vitest'; -import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import { SubscribeRequestSchema, UnsubscribeRequestSchema } from '@modelcontextprotocol/sdk/types.js'; +import type { McpServer } from '@modelcontextprotocol/server'; import { KnowledgeGraphManager, registerKnowledgeGraphResource, @@ -59,8 +58,8 @@ describe('knowledge-graph resource subscriptions', () => { return { mockServer, inner }; } - function handlerFor(inner: ReturnType['inner'], schema: unknown) { - const call = inner.setRequestHandler.mock.calls.find((c) => c[0] === schema); + function handlerFor(inner: ReturnType['inner'], method: string) { + const call = inner.setRequestHandler.mock.calls.find((c) => c[0] === method); if (!call) throw new Error('handler not registered'); return call[1] as (request: { params: { uri: string } }) => Promise; } @@ -80,9 +79,9 @@ describe('knowledge-graph resource subscriptions', () => { registerKnowledgeGraphSubscriptions(mockServer); - const schemas = inner.setRequestHandler.mock.calls.map((c) => c[0]); - expect(schemas).toContain(SubscribeRequestSchema); - expect(schemas).toContain(UnsubscribeRequestSchema); + const methods = inner.setRequestHandler.mock.calls.map((c) => c[0]); + expect(methods).toContain('resources/subscribe'); + expect(methods).toContain('resources/unsubscribe'); }); it('subscribe and unsubscribe handlers acknowledge with an empty result', async () => { @@ -91,7 +90,7 @@ describe('knowledge-graph resource subscriptions', () => { registerKnowledgeGraphSubscriptions(mockServer); const req = { params: { uri: 'memory://knowledge-graph' } }; - await expect(handlerFor(inner, SubscribeRequestSchema)(req)).resolves.toEqual({}); - await expect(handlerFor(inner, UnsubscribeRequestSchema)(req)).resolves.toEqual({}); + await expect(handlerFor(inner, 'resources/subscribe')(req)).resolves.toEqual({}); + await expect(handlerFor(inner, 'resources/unsubscribe')(req)).resolves.toEqual({}); }); }); diff --git a/src/memory/index.ts b/src/memory/index.ts index 9865c5318e..4643318728 100644 --- a/src/memory/index.ts +++ b/src/memory/index.ts @@ -1,8 +1,7 @@ #!/usr/bin/env node -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { SubscribeRequestSchema, UnsubscribeRequestSchema } from "@modelcontextprotocol/sdk/types.js"; +import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; +import { McpServer } from "@modelcontextprotocol/server"; import { z } from "zod"; import { promises as fs } from 'fs'; import path from 'path'; @@ -279,12 +278,12 @@ server.registerTool( { title: "Create Entities", description: "Create multiple new entities in the knowledge graph", - inputSchema: { + inputSchema: z.object({ entities: z.array(EntitySchema) - }, - outputSchema: { + }), + outputSchema: z.object({ entities: z.array(EntitySchema) - }, + }), annotations: { readOnlyHint: false, destructiveHint: false, @@ -308,12 +307,12 @@ server.registerTool( { title: "Create Relations", description: "Create multiple new relations between entities in the knowledge graph. Relations should be in active voice", - inputSchema: { + inputSchema: z.object({ relations: z.array(RelationSchema) - }, - outputSchema: { + }), + outputSchema: z.object({ relations: z.array(RelationSchema) - }, + }), annotations: { readOnlyHint: false, destructiveHint: false, @@ -337,18 +336,18 @@ server.registerTool( { title: "Add Observations", description: "Add new observations to existing entities in the knowledge graph", - inputSchema: { + inputSchema: z.object({ observations: z.array(z.object({ entityName: z.string().describe("The name of the entity to add the observations to"), contents: z.array(z.string()).describe("An array of observation contents to add") })) - }, - outputSchema: { + }), + outputSchema: z.object({ results: z.array(z.object({ entityName: z.string(), addedObservations: z.array(z.string()) })) - }, + }), annotations: { readOnlyHint: false, destructiveHint: false, @@ -372,13 +371,13 @@ server.registerTool( { title: "Delete Entities", description: "Delete multiple entities and their associated relations from the knowledge graph", - inputSchema: { + inputSchema: z.object({ entityNames: z.array(z.string()).describe("An array of entity names to delete") - }, - outputSchema: { + }), + outputSchema: z.object({ success: z.boolean(), message: z.string() - }, + }), annotations: { readOnlyHint: false, destructiveHint: true, @@ -402,16 +401,16 @@ server.registerTool( { title: "Delete Observations", description: "Delete specific observations from entities in the knowledge graph", - inputSchema: { + inputSchema: z.object({ deletions: z.array(z.object({ entityName: z.string().describe("The name of the entity containing the observations"), observations: z.array(z.string()).describe("An array of observations to delete") })) - }, - outputSchema: { + }), + outputSchema: z.object({ success: z.boolean(), message: z.string() - }, + }), annotations: { readOnlyHint: false, destructiveHint: true, @@ -435,13 +434,13 @@ server.registerTool( { title: "Delete Relations", description: "Delete multiple relations from the knowledge graph", - inputSchema: { + inputSchema: z.object({ relations: z.array(RelationSchema).describe("An array of relations to delete") - }, - outputSchema: { + }), + outputSchema: z.object({ success: z.boolean(), message: z.string() - }, + }), annotations: { readOnlyHint: false, destructiveHint: true, @@ -465,11 +464,11 @@ server.registerTool( { title: "Read Graph", description: "Read the entire knowledge graph", - inputSchema: {}, - outputSchema: { + inputSchema: z.object({}), + outputSchema: z.object({ entities: z.array(EntitySchema), relations: z.array(RelationSchema) - }, + }), annotations: { readOnlyHint: true, destructiveHint: false, @@ -492,13 +491,13 @@ server.registerTool( { title: "Search Nodes", description: "Search for nodes in the knowledge graph based on a query", - inputSchema: { + inputSchema: z.object({ query: z.string().describe("The search query to match against entity names, types, and observation content") - }, - outputSchema: { + }), + outputSchema: z.object({ entities: z.array(EntitySchema), relations: z.array(RelationSchema) - }, + }), annotations: { readOnlyHint: true, destructiveHint: false, @@ -521,13 +520,13 @@ server.registerTool( { title: "Open Nodes", description: "Open specific nodes in the knowledge graph by their names", - inputSchema: { + inputSchema: z.object({ names: z.array(z.string()).describe("An array of entity names to retrieve") - }, - outputSchema: { + }), + outputSchema: z.object({ entities: z.array(EntitySchema), relations: z.array(RelationSchema) - }, + }), annotations: { readOnlyHint: true, destructiveHint: false, @@ -575,11 +574,11 @@ export function registerKnowledgeGraphResource( // notifications/resources/updated when mutation tools change the graph. export function registerKnowledgeGraphSubscriptions(server: McpServer) { server.server.registerCapabilities({ resources: { subscribe: true } }); - server.server.setRequestHandler(SubscribeRequestSchema, async (request) => { + server.server.setRequestHandler("resources/subscribe", async (request) => { resourceSubscribers.add(request.params.uri); return {}; }); - server.server.setRequestHandler(UnsubscribeRequestSchema, async (request) => { + server.server.setRequestHandler("resources/unsubscribe", async (request) => { resourceSubscribers.delete(request.params.uri); return {}; }); diff --git a/src/memory/package.json b/src/memory/package.json index e32d25a3e2..4883d95f7f 100644 --- a/src/memory/package.json +++ b/src/memory/package.json @@ -25,7 +25,8 @@ "test": "vitest run --coverage" }, "dependencies": { - "@modelcontextprotocol/sdk": "^1.29.0" + "@modelcontextprotocol/server": "^2.0.0-alpha.3", + "@modelcontextprotocol/core": "^2.0.0-alpha.1" }, "devDependencies": { "@types/node": "^22", From 976c6a3e131af8c13108281e53142f62023549fa Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Wed, 1 Jul 2026 13:54:13 +0300 Subject: [PATCH 4/4] reduce formatting noise --- src/everything/__tests__/prompts.test.ts | 159 ++--- .../__tests__/registrations.test.ts | 94 +-- src/everything/__tests__/resources.test.ts | 240 +++---- src/everything/__tests__/server.test.ts | 24 +- src/everything/__tests__/tools.test.ts | 667 +++++++++--------- src/everything/resources/session.ts | 7 +- src/everything/tools/get-annotated-message.ts | 8 +- .../tools/trigger-url-elicitation.ts | 11 +- src/everything/vitest.config.ts | 12 +- src/filesystem/__tests__/roots-utils.test.ts | 2 +- .../__tests__/structured-content.test.ts | 4 +- src/filesystem/index.ts | 97 ++- src/filesystem/roots-utils.ts | 2 +- src/sequentialthinking/index.ts | 36 +- 14 files changed, 637 insertions(+), 726 deletions(-) diff --git a/src/everything/__tests__/prompts.test.ts b/src/everything/__tests__/prompts.test.ts index 8dca73e034..2e5fdbb340 100644 --- a/src/everything/__tests__/prompts.test.ts +++ b/src/everything/__tests__/prompts.test.ts @@ -1,9 +1,9 @@ -import { describe, it, expect, vi } from "vitest"; -import { McpServer } from "@modelcontextprotocol/server"; -import { registerSimplePrompt } from "../prompts/simple.js"; -import { registerArgumentsPrompt } from "../prompts/args.js"; -import { registerPromptWithCompletions } from "../prompts/completions.js"; -import { registerEmbeddedResourcePrompt } from "../prompts/resource.js"; +import { describe, it, expect, vi } from 'vitest'; +import { McpServer } from '@modelcontextprotocol/server'; +import { registerSimplePrompt } from '../prompts/simple.js'; +import { registerArgumentsPrompt } from '../prompts/args.js'; +import { registerPromptWithCompletions } from '../prompts/completions.js'; +import { registerEmbeddedResourcePrompt } from '../prompts/resource.js'; // Helper to capture registered prompt handlers function createMockServer() { @@ -20,22 +20,22 @@ function createMockServer() { return { mockServer, handlers, configs }; } -describe("Prompts", () => { - describe("simple-prompt", () => { - it("should return fixed message with no arguments", () => { +describe('Prompts', () => { + describe('simple-prompt', () => { + it('should return fixed message with no arguments', () => { const { mockServer, handlers } = createMockServer(); registerSimplePrompt(mockServer); - const handler = handlers.get("simple-prompt")!; + const handler = handlers.get('simple-prompt')!; const result = handler(); expect(result).toEqual({ messages: [ { - role: "user", + role: 'user', content: { - type: "text", - text: "This is a simple prompt without arguments.", + type: 'text', + text: 'This is a simple prompt without arguments.', }, }, ], @@ -43,144 +43,137 @@ describe("Prompts", () => { }); }); - describe("args-prompt", () => { - it("should include city in message", () => { + describe('args-prompt', () => { + it('should include city in message', () => { const { mockServer, handlers } = createMockServer(); registerArgumentsPrompt(mockServer); - const handler = handlers.get("args-prompt")!; - const result = handler({ city: "San Francisco" }); + const handler = handlers.get('args-prompt')!; + const result = handler({ city: 'San Francisco' }); - expect(result.messages[0].content.text).toBe( - "What's weather in San Francisco?" - ); + expect(result.messages[0].content.text).toBe("What's weather in San Francisco?"); }); - it("should include city and state in message", () => { + it('should include city and state in message', () => { const { mockServer, handlers } = createMockServer(); registerArgumentsPrompt(mockServer); - const handler = handlers.get("args-prompt")!; - const result = handler({ city: "San Francisco", state: "California" }); + const handler = handlers.get('args-prompt')!; + const result = handler({ city: 'San Francisco', state: 'California' }); expect(result.messages[0].content.text).toBe( "What's weather in San Francisco, California?" ); }); - it("should handle city only (optional state omitted)", () => { + it('should handle city only (optional state omitted)', () => { const { mockServer, handlers } = createMockServer(); registerArgumentsPrompt(mockServer); - const handler = handlers.get("args-prompt")!; - const result = handler({ city: "New York" }); + const handler = handlers.get('args-prompt')!; + const result = handler({ city: 'New York' }); - expect(result.messages[0].content.text).toBe( - "What's weather in New York?" - ); - expect(result.messages[0].content.text).not.toContain(","); - expect(result.messages[0].role).toBe("user"); - expect(result.messages[0].content.type).toBe("text"); + expect(result.messages[0].content.text).toBe("What's weather in New York?"); + expect(result.messages[0].content.text).not.toContain(','); + expect(result.messages[0].role).toBe('user'); + expect(result.messages[0].content.type).toBe('text'); }); }); - describe("completable-prompt", () => { - it("should generate promotion message with department and name", () => { + describe('completable-prompt', () => { + it('should generate promotion message with department and name', () => { const { mockServer, handlers } = createMockServer(); registerPromptWithCompletions(mockServer); - const handler = handlers.get("completable-prompt")!; - const result = handler({ department: "Engineering", name: "Alice" }); + const handler = handlers.get('completable-prompt')!; + const result = handler({ department: 'Engineering', name: 'Alice' }); expect(result.messages[0].content.text).toBe( - "Please promote Alice to the head of the Engineering team." + 'Please promote Alice to the head of the Engineering team.' ); }); - it("should work with different departments", () => { + it('should work with different departments', () => { const { mockServer, handlers } = createMockServer(); registerPromptWithCompletions(mockServer); - const handler = handlers.get("completable-prompt")!; + const handler = handlers.get('completable-prompt')!; - const salesResult = handler({ department: "Sales", name: "David" }); - expect(salesResult.messages[0].content.text).toContain("Sales"); - expect(salesResult.messages[0].content.text).toContain("David"); - expect(salesResult.messages[0].role).toBe("user"); + const salesResult = handler({ department: 'Sales', name: 'David' }); + expect(salesResult.messages[0].content.text).toContain('Sales'); + expect(salesResult.messages[0].content.text).toContain('David'); + expect(salesResult.messages[0].role).toBe('user'); - const marketingResult = handler({ - department: "Marketing", - name: "Grace", - }); - expect(marketingResult.messages[0].content.text).toContain("Marketing"); - expect(marketingResult.messages[0].content.text).toContain("Grace"); + const marketingResult = handler({ department: 'Marketing', name: 'Grace' }); + expect(marketingResult.messages[0].content.text).toContain('Marketing'); + expect(marketingResult.messages[0].content.text).toContain('Grace'); }); }); - describe("resource-prompt", () => { - it("should return text resource reference", () => { + describe('resource-prompt', () => { + it('should return text resource reference', () => { const { mockServer, handlers } = createMockServer(); registerEmbeddedResourcePrompt(mockServer); - const handler = handlers.get("resource-prompt")!; - const result = handler({ resourceType: "Text", resourceId: "1" }); + const handler = handlers.get('resource-prompt')!; + const result = handler({ resourceType: 'Text', resourceId: '1' }); expect(result.messages).toHaveLength(2); - expect(result.messages[0].content.text).toContain("Text"); - expect(result.messages[0].content.text).toContain("1"); - expect(result.messages[1].content.type).toBe("resource"); - expect(result.messages[1].content.resource.uri).toContain("text/1"); + expect(result.messages[0].content.text).toContain('Text'); + expect(result.messages[0].content.text).toContain('1'); + expect(result.messages[1].content.type).toBe('resource'); + expect(result.messages[1].content.resource.uri).toContain('text/1'); }); - it("should return blob resource reference", () => { + it('should return blob resource reference', () => { const { mockServer, handlers } = createMockServer(); registerEmbeddedResourcePrompt(mockServer); - const handler = handlers.get("resource-prompt")!; - const result = handler({ resourceType: "Blob", resourceId: "5" }); + const handler = handlers.get('resource-prompt')!; + const result = handler({ resourceType: 'Blob', resourceId: '5' }); - expect(result.messages[0].content.text).toContain("Blob"); - expect(result.messages[1].content.resource.uri).toContain("blob/5"); + expect(result.messages[0].content.text).toContain('Blob'); + expect(result.messages[1].content.resource.uri).toContain('blob/5'); }); - it("should reject invalid resource type", () => { + it('should reject invalid resource type', () => { const { mockServer, handlers } = createMockServer(); registerEmbeddedResourcePrompt(mockServer); - const handler = handlers.get("resource-prompt")!; - expect(() => - handler({ resourceType: "Invalid", resourceId: "1" }) - ).toThrow("Invalid resourceType"); + const handler = handlers.get('resource-prompt')!; + expect(() => handler({ resourceType: 'Invalid', resourceId: '1' })).toThrow( + 'Invalid resourceType' + ); }); - it("should reject invalid resource ID", () => { + it('should reject invalid resource ID', () => { const { mockServer, handlers } = createMockServer(); registerEmbeddedResourcePrompt(mockServer); - const handler = handlers.get("resource-prompt")!; - expect(() => handler({ resourceType: "Text", resourceId: "-1" })).toThrow( - "Invalid resourceId" + const handler = handlers.get('resource-prompt')!; + expect(() => handler({ resourceType: 'Text', resourceId: '-1' })).toThrow( + 'Invalid resourceId' + ); + expect(() => handler({ resourceType: 'Text', resourceId: '0' })).toThrow( + 'Invalid resourceId' ); - expect(() => handler({ resourceType: "Text", resourceId: "0" })).toThrow( - "Invalid resourceId" + expect(() => handler({ resourceType: 'Text', resourceId: 'abc' })).toThrow( + 'Invalid resourceId' ); - expect(() => - handler({ resourceType: "Text", resourceId: "abc" }) - ).toThrow("Invalid resourceId"); }); - it("should include both intro text and resource messages", () => { + it('should include both intro text and resource messages', () => { const { mockServer, handlers } = createMockServer(); registerEmbeddedResourcePrompt(mockServer); - const handler = handlers.get("resource-prompt")!; - const result = handler({ resourceType: "Text", resourceId: "3" }); + const handler = handlers.get('resource-prompt')!; + const result = handler({ resourceType: 'Text', resourceId: '3' }); expect(result.messages).toHaveLength(2); - expect(result.messages[0].role).toBe("user"); - expect(result.messages[0].content.type).toBe("text"); - expect(result.messages[1].role).toBe("user"); - expect(result.messages[1].content.type).toBe("resource"); + expect(result.messages[0].role).toBe('user'); + expect(result.messages[0].content.type).toBe('text'); + expect(result.messages[1].role).toBe('user'); + expect(result.messages[1].content.type).toBe('resource'); }); }); }); diff --git a/src/everything/__tests__/registrations.test.ts b/src/everything/__tests__/registrations.test.ts index ba30682cf8..3293cc4a26 100644 --- a/src/everything/__tests__/registrations.test.ts +++ b/src/everything/__tests__/registrations.test.ts @@ -1,5 +1,5 @@ -import { describe, it, expect, vi } from "vitest"; -import { McpServer } from "@modelcontextprotocol/server"; +import { describe, it, expect, vi } from 'vitest'; +import { McpServer } from '@modelcontextprotocol/server'; // Create mock server function createMockServer() { @@ -16,10 +16,10 @@ function createMockServer() { } as unknown as McpServer; } -describe("Registration Index Files", () => { - describe("tools/index.ts", () => { - it("should register all standard tools", async () => { - const { registerTools } = await import("../tools/index.js"); +describe('Registration Index Files', () => { + describe('tools/index.ts', () => { + it('should register all standard tools', async () => { + const { registerTools } = await import('../tools/index.js'); const mockServer = createMockServer(); registerTools(mockServer); @@ -31,22 +31,22 @@ describe("Registration Index Files", () => { const registeredTools = (mockServer.registerTool as any).mock.calls.map( (call: any[]) => call[0] ); - expect(registeredTools).toContain("echo"); - expect(registeredTools).toContain("get-sum"); - expect(registeredTools).toContain("get-env"); - expect(registeredTools).toContain("get-tiny-image"); - expect(registeredTools).toContain("get-structured-content"); - expect(registeredTools).toContain("get-annotated-message"); - expect(registeredTools).toContain("trigger-long-running-operation"); - expect(registeredTools).toContain("get-resource-links"); - expect(registeredTools).toContain("get-resource-reference"); - expect(registeredTools).toContain("gzip-file-as-resource"); - expect(registeredTools).toContain("toggle-simulated-logging"); - expect(registeredTools).toContain("toggle-subscriber-updates"); + expect(registeredTools).toContain('echo'); + expect(registeredTools).toContain('get-sum'); + expect(registeredTools).toContain('get-env'); + expect(registeredTools).toContain('get-tiny-image'); + expect(registeredTools).toContain('get-structured-content'); + expect(registeredTools).toContain('get-annotated-message'); + expect(registeredTools).toContain('trigger-long-running-operation'); + expect(registeredTools).toContain('get-resource-links'); + expect(registeredTools).toContain('get-resource-reference'); + expect(registeredTools).toContain('gzip-file-as-resource'); + expect(registeredTools).toContain('toggle-simulated-logging'); + expect(registeredTools).toContain('toggle-subscriber-updates'); }); - it("should register conditional tools based on capabilities", async () => { - const { registerConditionalTools } = await import("../tools/index.js"); + it('should register conditional tools based on capabilities', async () => { + const { registerConditionalTools } = await import('../tools/index.js'); // Server with all capabilities including experimental tasks API const mockServerWithCapabilities = { @@ -75,14 +75,14 @@ describe("Registration Index Files", () => { const registeredTools = ( mockServerWithCapabilities.registerTool as any ).mock.calls.map((call: any[]) => call[0]); - expect(registeredTools).toContain("get-roots-list"); - expect(registeredTools).toContain("trigger-elicitation-request"); - expect(registeredTools).toContain("trigger-url-elicitation"); - expect(registeredTools).toContain("trigger-sampling-request"); + expect(registeredTools).toContain('get-roots-list'); + expect(registeredTools).toContain('trigger-elicitation-request'); + expect(registeredTools).toContain('trigger-url-elicitation'); + expect(registeredTools).toContain('trigger-sampling-request'); }); - it("should not register conditional tools when capabilities missing", async () => { - const { registerConditionalTools } = await import("../tools/index.js"); + it('should not register conditional tools when capabilities missing', async () => { + const { registerConditionalTools } = await import('../tools/index.js'); const mockServerNoCapabilities = { registerTool: vi.fn(), @@ -103,9 +103,9 @@ describe("Registration Index Files", () => { }); }); - describe("prompts/index.ts", () => { - it("should register all prompts", async () => { - const { registerPrompts } = await import("../prompts/index.js"); + describe('prompts/index.ts', () => { + it('should register all prompts', async () => { + const { registerPrompts } = await import('../prompts/index.js'); const mockServer = createMockServer(); registerPrompts(mockServer); @@ -113,39 +113,39 @@ describe("Registration Index Files", () => { // Should register 4 prompts expect(mockServer.registerPrompt).toHaveBeenCalledTimes(4); - const registeredPrompts = ( - mockServer.registerPrompt as any - ).mock.calls.map((call: any[]) => call[0]); - expect(registeredPrompts).toContain("simple-prompt"); - expect(registeredPrompts).toContain("args-prompt"); - expect(registeredPrompts).toContain("completable-prompt"); - expect(registeredPrompts).toContain("resource-prompt"); + const registeredPrompts = (mockServer.registerPrompt as any).mock.calls.map( + (call: any[]) => call[0] + ); + expect(registeredPrompts).toContain('simple-prompt'); + expect(registeredPrompts).toContain('args-prompt'); + expect(registeredPrompts).toContain('completable-prompt'); + expect(registeredPrompts).toContain('resource-prompt'); }); }); - describe("resources/index.ts", () => { - it("should register resource templates", async () => { - const { registerResources } = await import("../resources/index.js"); + describe('resources/index.ts', () => { + it('should register resource templates', async () => { + const { registerResources } = await import('../resources/index.js'); const mockServer = createMockServer(); registerResources(mockServer); // Should register at least the 2 resource templates (text and blob) plus file resources expect(mockServer.registerResource).toHaveBeenCalled(); - const registeredResources = ( - mockServer.registerResource as any - ).mock.calls.map((call: any[]) => call[0]); - expect(registeredResources).toContain("Dynamic Text Resource"); - expect(registeredResources).toContain("Dynamic Blob Resource"); + const registeredResources = (mockServer.registerResource as any).mock.calls.map( + (call: any[]) => call[0] + ); + expect(registeredResources).toContain('Dynamic Text Resource'); + expect(registeredResources).toContain('Dynamic Blob Resource'); }); - it("should read instructions from file", async () => { - const { readInstructions } = await import("../resources/index.js"); + it('should read instructions from file', async () => { + const { readInstructions } = await import('../resources/index.js'); const instructions = readInstructions(); // Should return a string (either content or error message) - expect(typeof instructions).toBe("string"); + expect(typeof instructions).toBe('string'); expect(instructions.length).toBeGreaterThan(0); }); }); diff --git a/src/everything/__tests__/resources.test.ts b/src/everything/__tests__/resources.test.ts index 1cf1cdde6d..f7b4037b65 100644 --- a/src/everything/__tests__/resources.test.ts +++ b/src/everything/__tests__/resources.test.ts @@ -1,5 +1,5 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; -import { McpServer, ResourceTemplate } from "@modelcontextprotocol/server"; +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { McpServer, ResourceTemplate } from '@modelcontextprotocol/server'; import { textResource, blobResource, @@ -12,71 +12,63 @@ import { resourceIdForPromptCompleter, resourceIdForResourceTemplateCompleter, registerResourceTemplates, -} from "../resources/templates.js"; +} from '../resources/templates.js'; import { getSessionResourceURI, registerSessionResource, -} from "../resources/session.js"; -import { registerFileResources } from "../resources/files.js"; +} from '../resources/session.js'; +import { registerFileResources } from '../resources/files.js'; import { setSubscriptionHandlers, beginSimulatedResourceUpdates, stopSimulatedResourceUpdates, -} from "../resources/subscriptions.js"; +} from '../resources/subscriptions.js'; -describe("Resource Templates", () => { - describe("Constants", () => { - it("should include both types in RESOURCE_TYPES array", () => { +describe('Resource Templates', () => { + describe('Constants', () => { + it('should include both types in RESOURCE_TYPES array', () => { expect(RESOURCE_TYPES).toContain(RESOURCE_TYPE_TEXT); expect(RESOURCE_TYPES).toContain(RESOURCE_TYPE_BLOB); expect(RESOURCE_TYPES).toHaveLength(2); }); }); - describe("textResourceUri", () => { - it("should create URL for text resource", () => { + describe('textResourceUri', () => { + it('should create URL for text resource', () => { const uri = textResourceUri(1); - expect(uri.toString()).toBe("demo://resource/dynamic/text/1"); + expect(uri.toString()).toBe('demo://resource/dynamic/text/1'); }); - it("should handle different resource IDs", () => { - expect(textResourceUri(5).toString()).toBe( - "demo://resource/dynamic/text/5" - ); - expect(textResourceUri(100).toString()).toBe( - "demo://resource/dynamic/text/100" - ); + it('should handle different resource IDs', () => { + expect(textResourceUri(5).toString()).toBe('demo://resource/dynamic/text/5'); + expect(textResourceUri(100).toString()).toBe('demo://resource/dynamic/text/100'); }); }); - describe("blobResourceUri", () => { - it("should create URL for blob resource", () => { + describe('blobResourceUri', () => { + it('should create URL for blob resource', () => { const uri = blobResourceUri(1); - expect(uri.toString()).toBe("demo://resource/dynamic/blob/1"); + expect(uri.toString()).toBe('demo://resource/dynamic/blob/1'); }); - it("should handle different resource IDs", () => { - expect(blobResourceUri(5).toString()).toBe( - "demo://resource/dynamic/blob/5" - ); - expect(blobResourceUri(100).toString()).toBe( - "demo://resource/dynamic/blob/100" - ); + it('should handle different resource IDs', () => { + expect(blobResourceUri(5).toString()).toBe('demo://resource/dynamic/blob/5'); + expect(blobResourceUri(100).toString()).toBe('demo://resource/dynamic/blob/100'); }); }); - describe("textResource", () => { - it("should create text resource with correct structure", () => { + describe('textResource', () => { + it('should create text resource with correct structure', () => { const uri = textResourceUri(1); const resource = textResource(uri, 1); expect(resource.uri).toBe(uri.toString()); - expect(resource.mimeType).toBe("text/plain"); - expect(resource.text).toContain("Resource 1"); - expect(resource.text).toContain("plaintext"); + expect(resource.mimeType).toBe('text/plain'); + expect(resource.text).toContain('Resource 1'); + expect(resource.text).toContain('plaintext'); }); - it("should include timestamp in content", () => { + it('should include timestamp in content', () => { const uri = textResourceUri(2); const resource = textResource(uri, 2); @@ -85,78 +77,70 @@ describe("Resource Templates", () => { }); }); - describe("blobResource", () => { - it("should create blob resource with correct structure", () => { + describe('blobResource', () => { + it('should create blob resource with correct structure', () => { const uri = blobResourceUri(1); const resource = blobResource(uri, 1); expect(resource.uri).toBe(uri.toString()); - expect(resource.mimeType).toBe("text/plain"); + expect(resource.mimeType).toBe('text/plain'); expect(resource.blob).toBeDefined(); }); - it("should create valid base64 encoded content", () => { + it('should create valid base64 encoded content', () => { const uri = blobResourceUri(3); const resource = blobResource(uri, 3); // Decode and verify content - const decoded = Buffer.from(resource.blob, "base64").toString(); - expect(decoded).toContain("Resource 3"); - expect(decoded).toContain("base64 blob"); + const decoded = Buffer.from(resource.blob, 'base64').toString(); + expect(decoded).toContain('Resource 3'); + expect(decoded).toContain('base64 blob'); }); }); - describe("resourceTypeCompleter", () => { - it("should be defined as a completable schema", () => { + describe('resourceTypeCompleter', () => { + it('should be defined as a completable schema', () => { // The completer is a zod schema wrapped with completable expect(resourceTypeCompleter).toBeDefined(); // It should have the zod parse method - expect(typeof (resourceTypeCompleter as any).parse).toBe("function"); + expect(typeof (resourceTypeCompleter as any).parse).toBe('function'); }); - it("should validate string resource types", () => { + it('should validate string resource types', () => { // Test that valid strings pass validation - expect(() => (resourceTypeCompleter as any).parse("Text")).not.toThrow(); - expect(() => (resourceTypeCompleter as any).parse("Blob")).not.toThrow(); + expect(() => (resourceTypeCompleter as any).parse('Text')).not.toThrow(); + expect(() => (resourceTypeCompleter as any).parse('Blob')).not.toThrow(); }); }); - describe("resourceIdForPromptCompleter", () => { - it("should be defined as a completable schema", () => { + describe('resourceIdForPromptCompleter', () => { + it('should be defined as a completable schema', () => { expect(resourceIdForPromptCompleter).toBeDefined(); - expect(typeof (resourceIdForPromptCompleter as any).parse).toBe( - "function" - ); + expect(typeof (resourceIdForPromptCompleter as any).parse).toBe('function'); }); - it("should validate string IDs", () => { + it('should validate string IDs', () => { // Test that valid strings pass validation - expect(() => - (resourceIdForPromptCompleter as any).parse("1") - ).not.toThrow(); - expect(() => - (resourceIdForPromptCompleter as any).parse("100") - ).not.toThrow(); + expect(() => (resourceIdForPromptCompleter as any).parse('1')).not.toThrow(); + expect(() => (resourceIdForPromptCompleter as any).parse('100')).not.toThrow(); }); }); - describe("resourceIdForResourceTemplateCompleter", () => { - it("should validate positive integer IDs", () => { - expect(resourceIdForResourceTemplateCompleter("1")).toEqual(["1"]); - expect(resourceIdForResourceTemplateCompleter("50")).toEqual(["50"]); + describe('resourceIdForResourceTemplateCompleter', () => { + it('should validate positive integer IDs', () => { + expect(resourceIdForResourceTemplateCompleter('1')).toEqual(['1']); + expect(resourceIdForResourceTemplateCompleter('50')).toEqual(['50']); }); - it("should reject invalid IDs", () => { - expect(resourceIdForResourceTemplateCompleter("0")).toEqual([]); - expect(resourceIdForResourceTemplateCompleter("-5")).toEqual([]); - expect(resourceIdForResourceTemplateCompleter("not-a-number")).toEqual( - [] - ); + it('should reject invalid IDs', () => { + expect(resourceIdForResourceTemplateCompleter('0')).toEqual([]); + expect(resourceIdForResourceTemplateCompleter('-5')).toEqual([]); + expect(resourceIdForResourceTemplateCompleter('not-a-number')).toEqual([]); }); }); - describe("registerResourceTemplates", () => { - it("should register text and blob resource templates", () => { + describe('registerResourceTemplates', () => { + it('should register text and blob resource templates', () => { const registeredResources: any[] = []; const mockServer = { @@ -171,40 +155,36 @@ describe("Resource Templates", () => { // Check text resource registration const textRegistration = registeredResources.find((r) => - r[0].includes("Text") + r[0].includes('Text') ); expect(textRegistration).toBeDefined(); expect(textRegistration[1]).toBeInstanceOf(ResourceTemplate); // Check blob resource registration const blobRegistration = registeredResources.find((r) => - r[0].includes("Blob") + r[0].includes('Blob') ); expect(blobRegistration).toBeDefined(); }); }); }); -describe("Session Resources", () => { - describe("getSessionResourceURI", () => { - it("should generate correct URI for resource name", () => { - expect(getSessionResourceURI("test")).toBe( - "demo://resource/session/test" - ); +describe('Session Resources', () => { + describe('getSessionResourceURI', () => { + it('should generate correct URI for resource name', () => { + expect(getSessionResourceURI('test')).toBe('demo://resource/session/test'); }); - it("should handle various resource names", () => { - expect(getSessionResourceURI("my-file")).toBe( - "demo://resource/session/my-file" - ); - expect(getSessionResourceURI("document_123")).toBe( - "demo://resource/session/document_123" + it('should handle various resource names', () => { + expect(getSessionResourceURI('my-file')).toBe('demo://resource/session/my-file'); + expect(getSessionResourceURI('document_123')).toBe( + 'demo://resource/session/document_123' ); }); }); - describe("registerSessionResource", () => { - it("should register text resource and return resource link", () => { + describe('registerSessionResource', () => { + it('should register text resource and return resource link', () => { const registrations: any[] = []; const mockServer = { registerResource: vi.fn((...args) => { @@ -213,58 +193,53 @@ describe("Session Resources", () => { } as unknown as McpServer; const resource = { - uri: "demo://resource/session/test-file", - name: "test-file", - mimeType: "text/plain", - description: "A test file", + uri: 'demo://resource/session/test-file', + name: 'test-file', + mimeType: 'text/plain', + description: 'A test file', }; const result = registerSessionResource( mockServer, resource, - "text", - "Hello, World!" + 'text', + 'Hello, World!' ); - expect(result.type).toBe("resource_link"); + expect(result.type).toBe('resource_link'); expect(result.uri).toBe(resource.uri); expect(result.name).toBe(resource.name); expect(mockServer.registerResource).toHaveBeenCalledWith( - "test-file", - "demo://resource/session/test-file", + 'test-file', + 'demo://resource/session/test-file', expect.objectContaining({ - mimeType: "text/plain", - description: "A test file", + mimeType: 'text/plain', + description: 'A test file', }), expect.any(Function) ); }); - it("should register blob resource correctly", () => { + it('should register blob resource correctly', () => { const mockServer = { registerResource: vi.fn(), } as unknown as McpServer; const resource = { - uri: "demo://resource/session/binary-file", - name: "binary-file", - mimeType: "application/octet-stream", + uri: 'demo://resource/session/binary-file', + name: 'binary-file', + mimeType: 'application/octet-stream', }; - const blobContent = Buffer.from("binary data").toString("base64"); - const result = registerSessionResource( - mockServer, - resource, - "blob", - blobContent - ); + const blobContent = Buffer.from('binary data').toString('base64'); + const result = registerSessionResource(mockServer, resource, 'blob', blobContent); - expect(result.type).toBe("resource_link"); + expect(result.type).toBe('resource_link'); expect(mockServer.registerResource).toHaveBeenCalled(); }); - it("should return resource handler that provides correct content", async () => { + it('should return resource handler that provides correct content', async () => { let capturedHandler: Function | null = null; const mockServer = { registerResource: vi.fn((_name, _uri, _config, handler) => { @@ -273,31 +248,26 @@ describe("Session Resources", () => { } as unknown as McpServer; const resource = { - uri: "demo://resource/session/content-test", - name: "content-test", - mimeType: "text/plain", + uri: 'demo://resource/session/content-test', + name: 'content-test', + mimeType: 'text/plain', }; - registerSessionResource( - mockServer, - resource, - "text", - "Test content here" - ); + registerSessionResource(mockServer, resource, 'text', 'Test content here'); expect(capturedHandler).not.toBeNull(); const handlerResult = await capturedHandler!(new URL(resource.uri)); expect(handlerResult.contents).toHaveLength(1); - expect(handlerResult.contents[0].text).toBe("Test content here"); - expect(handlerResult.contents[0].mimeType).toBe("text/plain"); + expect(handlerResult.contents[0].text).toBe('Test content here'); + expect(handlerResult.contents[0].mimeType).toBe('text/plain'); }); }); }); -describe("File Resources", () => { - describe("registerFileResources", () => { - it("should register file resources when docs directory exists", () => { +describe('File Resources', () => { + describe('registerFileResources', () => { + it('should register file resources when docs directory exists', () => { const mockServer = { registerResource: vi.fn(), } as unknown as McpServer; @@ -311,9 +281,9 @@ describe("File Resources", () => { }); }); -describe("Subscriptions", () => { - describe("setSubscriptionHandlers", () => { - it("should set request handlers on server", () => { +describe('Subscriptions', () => { + describe('setSubscriptionHandlers', () => { + it('should set request handlers on server', () => { const mockServer = { server: { setRequestHandler: vi.fn(), @@ -328,13 +298,13 @@ describe("Subscriptions", () => { }); }); - describe("simulated resource updates lifecycle", () => { + describe('simulated resource updates lifecycle', () => { afterEach(() => { // Clean up any intervals - stopSimulatedResourceUpdates("lifecycle-test-session"); + stopSimulatedResourceUpdates('lifecycle-test-session'); }); - it("should start and stop updates without errors", () => { + it('should start and stop updates without errors', () => { const mockServer = { server: { notification: vi.fn(), @@ -342,12 +312,12 @@ describe("Subscriptions", () => { } as unknown as McpServer; // Start updates - should work for both defined and undefined sessionId - beginSimulatedResourceUpdates(mockServer, "lifecycle-test-session"); + beginSimulatedResourceUpdates(mockServer, 'lifecycle-test-session'); beginSimulatedResourceUpdates(mockServer, undefined); // Stop updates - should handle all cases gracefully - stopSimulatedResourceUpdates("lifecycle-test-session"); - stopSimulatedResourceUpdates("non-existent-session"); + stopSimulatedResourceUpdates('lifecycle-test-session'); + stopSimulatedResourceUpdates('non-existent-session'); stopSimulatedResourceUpdates(undefined); // If we got here without throwing, the lifecycle works correctly diff --git a/src/everything/__tests__/server.test.ts b/src/everything/__tests__/server.test.ts index c7010a0627..e7985dd982 100644 --- a/src/everything/__tests__/server.test.ts +++ b/src/everything/__tests__/server.test.ts @@ -1,35 +1,35 @@ -import { describe, it, expect, vi } from "vitest"; -import { createServer } from "../server/index.js"; +import { describe, it, expect, vi } from 'vitest'; +import { createServer } from '../server/index.js'; -describe("Server Factory", () => { - describe("createServer", () => { - it("should return a ServerFactoryResponse object", () => { +describe('Server Factory', () => { + describe('createServer', () => { + it('should return a ServerFactoryResponse object', () => { const result = createServer(); - expect(result).toHaveProperty("server"); - expect(result).toHaveProperty("cleanup"); + expect(result).toHaveProperty('server'); + expect(result).toHaveProperty('cleanup'); }); - it("should return a cleanup function", () => { + it('should return a cleanup function', () => { const { cleanup } = createServer(); - expect(typeof cleanup).toBe("function"); + expect(typeof cleanup).toBe('function'); }); - it("should create an McpServer instance", () => { + it('should create an McpServer instance', () => { const { server } = createServer(); expect(server).toBeDefined(); expect(server.server).toBeDefined(); }); - it("should have an oninitialized handler set", () => { + it('should have an oninitialized handler set', () => { const { server } = createServer(); expect(server.server.oninitialized).toBeDefined(); }); - it("should allow multiple servers to be created", () => { + it('should allow multiple servers to be created', () => { const result1 = createServer(); const result2 = createServer(); diff --git a/src/everything/__tests__/tools.test.ts b/src/everything/__tests__/tools.test.ts index 85314c8bbe..f20c4d0745 100644 --- a/src/everything/__tests__/tools.test.ts +++ b/src/everything/__tests__/tools.test.ts @@ -1,27 +1,24 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; -import { McpServer } from "@modelcontextprotocol/server"; -import { registerEchoTool, EchoSchema } from "../tools/echo.js"; -import { registerGetSumTool } from "../tools/get-sum.js"; -import { registerGetEnvTool } from "../tools/get-env.js"; -import { - registerGetTinyImageTool, - MCP_TINY_IMAGE, -} from "../tools/get-tiny-image.js"; -import { registerGetStructuredContentTool } from "../tools/get-structured-content.js"; -import { registerGetAnnotatedMessageTool } from "../tools/get-annotated-message.js"; -import { registerTriggerLongRunningOperationTool } from "../tools/trigger-long-running-operation.js"; -import { registerGetResourceLinksTool } from "../tools/get-resource-links.js"; -import { registerGetResourceReferenceTool } from "../tools/get-resource-reference.js"; -import { registerToggleSimulatedLoggingTool } from "../tools/toggle-simulated-logging.js"; -import { registerToggleSubscriberUpdatesTool } from "../tools/toggle-subscriber-updates.js"; -import { registerTriggerSamplingRequestTool } from "../tools/trigger-sampling-request.js"; -import { registerTriggerElicitationRequestTool } from "../tools/trigger-elicitation-request.js"; +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { McpServer } from '@modelcontextprotocol/server'; +import { registerEchoTool, EchoSchema } from '../tools/echo.js'; +import { registerGetSumTool } from '../tools/get-sum.js'; +import { registerGetEnvTool } from '../tools/get-env.js'; +import { registerGetTinyImageTool, MCP_TINY_IMAGE } from '../tools/get-tiny-image.js'; +import { registerGetStructuredContentTool } from '../tools/get-structured-content.js'; +import { registerGetAnnotatedMessageTool } from '../tools/get-annotated-message.js'; +import { registerTriggerLongRunningOperationTool } from '../tools/trigger-long-running-operation.js'; +import { registerGetResourceLinksTool } from '../tools/get-resource-links.js'; +import { registerGetResourceReferenceTool } from '../tools/get-resource-reference.js'; +import { registerToggleSimulatedLoggingTool } from '../tools/toggle-simulated-logging.js'; +import { registerToggleSubscriberUpdatesTool } from '../tools/toggle-subscriber-updates.js'; +import { registerTriggerSamplingRequestTool } from '../tools/trigger-sampling-request.js'; +import { registerTriggerElicitationRequestTool } from '../tools/trigger-elicitation-request.js'; import { registerTriggerUrlElicitationTool, __resetIssuedErrorPathElicitations, -} from "../tools/trigger-url-elicitation.js"; -import { registerGetRootsListTool } from "../tools/get-roots-list.js"; -import { registerGZipFileAsResourceTool } from "../tools/gzip-file-as-resource.js"; +} from '../tools/trigger-url-elicitation.js'; +import { registerGetRootsListTool } from '../tools/get-roots-list.js'; +import { registerGZipFileAsResourceTool } from '../tools/gzip-file-as-resource.js'; // Helper to capture registered tool handlers function createMockServer() { @@ -44,341 +41,322 @@ function createMockServer() { return { mockServer, handlers, configs }; } -describe("Tools", () => { - describe("echo", () => { - it("should echo back the message", async () => { +describe('Tools', () => { + describe('echo', () => { + it('should echo back the message', async () => { const { mockServer, handlers } = createMockServer(); registerEchoTool(mockServer); - const handler = handlers.get("echo")!; - const result = await handler({ message: "Hello, World!" }); + const handler = handlers.get('echo')!; + const result = await handler({ message: 'Hello, World!' }); expect(result).toEqual({ - content: [{ type: "text", text: "Echo: Hello, World!" }], + content: [{ type: 'text', text: 'Echo: Hello, World!' }], }); }); - it("should handle empty message", async () => { + it('should handle empty message', async () => { const { mockServer, handlers } = createMockServer(); registerEchoTool(mockServer); - const handler = handlers.get("echo")!; - const result = await handler({ message: "" }); + const handler = handlers.get('echo')!; + const result = await handler({ message: '' }); expect(result).toEqual({ - content: [{ type: "text", text: "Echo: " }], + content: [{ type: 'text', text: 'Echo: ' }], }); }); - it("should reject invalid input", async () => { + it('should reject invalid input', async () => { const { mockServer, handlers } = createMockServer(); registerEchoTool(mockServer); - const handler = handlers.get("echo")!; + const handler = handlers.get('echo')!; await expect(handler({})).rejects.toThrow(); await expect(handler({ message: 123 })).rejects.toThrow(); }); }); - describe("EchoSchema", () => { - it("should validate correct input", () => { - const result = EchoSchema.parse({ message: "test" }); - expect(result).toEqual({ message: "test" }); + describe('EchoSchema', () => { + it('should validate correct input', () => { + const result = EchoSchema.parse({ message: 'test' }); + expect(result).toEqual({ message: 'test' }); }); - it("should reject missing message", () => { + it('should reject missing message', () => { expect(() => EchoSchema.parse({})).toThrow(); }); - it("should reject non-string message", () => { + it('should reject non-string message', () => { expect(() => EchoSchema.parse({ message: 123 })).toThrow(); }); }); - describe("get-sum", () => { - it("should calculate sum of two positive numbers", async () => { + describe('get-sum', () => { + it('should calculate sum of two positive numbers', async () => { const { mockServer, handlers } = createMockServer(); registerGetSumTool(mockServer); - const handler = handlers.get("get-sum")!; + const handler = handlers.get('get-sum')!; const result = await handler({ a: 5, b: 3 }); expect(result).toEqual({ - content: [{ type: "text", text: "The sum of 5 and 3 is 8." }], + content: [{ type: 'text', text: 'The sum of 5 and 3 is 8.' }], }); }); - it("should calculate sum with negative numbers", async () => { + it('should calculate sum with negative numbers', async () => { const { mockServer, handlers } = createMockServer(); registerGetSumTool(mockServer); - const handler = handlers.get("get-sum")!; + const handler = handlers.get('get-sum')!; const result = await handler({ a: -5, b: 3 }); expect(result).toEqual({ - content: [{ type: "text", text: "The sum of -5 and 3 is -2." }], + content: [{ type: 'text', text: 'The sum of -5 and 3 is -2.' }], }); }); - it("should calculate sum with zero", async () => { + it('should calculate sum with zero', async () => { const { mockServer, handlers } = createMockServer(); registerGetSumTool(mockServer); - const handler = handlers.get("get-sum")!; + const handler = handlers.get('get-sum')!; const result = await handler({ a: 0, b: 0 }); expect(result).toEqual({ - content: [{ type: "text", text: "The sum of 0 and 0 is 0." }], + content: [{ type: 'text', text: 'The sum of 0 and 0 is 0.' }], }); }); - it("should handle floating point numbers", async () => { + it('should handle floating point numbers', async () => { const { mockServer, handlers } = createMockServer(); registerGetSumTool(mockServer); - const handler = handlers.get("get-sum")!; + const handler = handlers.get('get-sum')!; const result = await handler({ a: 1.5, b: 2.5 }); expect(result).toEqual({ - content: [{ type: "text", text: "The sum of 1.5 and 2.5 is 4." }], + content: [{ type: 'text', text: 'The sum of 1.5 and 2.5 is 4.' }], }); }); - it("should reject invalid input", async () => { + it('should reject invalid input', async () => { const { mockServer, handlers } = createMockServer(); registerGetSumTool(mockServer); - const handler = handlers.get("get-sum")!; + const handler = handlers.get('get-sum')!; await expect(handler({})).rejects.toThrow(); - await expect(handler({ a: "not a number", b: 5 })).rejects.toThrow(); + await expect(handler({ a: 'not a number', b: 5 })).rejects.toThrow(); await expect(handler({ a: 5 })).rejects.toThrow(); }); }); - describe("get-env", () => { - it("should return all environment variables as JSON", async () => { + describe('get-env', () => { + it('should return all environment variables as JSON', async () => { const { mockServer, handlers } = createMockServer(); registerGetEnvTool(mockServer); - const handler = handlers.get("get-env")!; - process.env.TEST_VAR_EVERYTHING = "test_value"; + const handler = handlers.get('get-env')!; + process.env.TEST_VAR_EVERYTHING = 'test_value'; const result = await handler({}); expect(result.content).toHaveLength(1); - expect(result.content[0].type).toBe("text"); + expect(result.content[0].type).toBe('text'); const envJson = JSON.parse(result.content[0].text); - expect(envJson.TEST_VAR_EVERYTHING).toBe("test_value"); + expect(envJson.TEST_VAR_EVERYTHING).toBe('test_value'); delete process.env.TEST_VAR_EVERYTHING; }); - it("should return valid JSON", async () => { + it('should return valid JSON', async () => { const { mockServer, handlers } = createMockServer(); registerGetEnvTool(mockServer); - const handler = handlers.get("get-env")!; + const handler = handlers.get('get-env')!; const result = await handler({}); expect(() => JSON.parse(result.content[0].text)).not.toThrow(); }); }); - describe("get-tiny-image", () => { - it("should return image content with text descriptions", async () => { + describe('get-tiny-image', () => { + it('should return image content with text descriptions', async () => { const { mockServer, handlers } = createMockServer(); registerGetTinyImageTool(mockServer); - const handler = handlers.get("get-tiny-image")!; + const handler = handlers.get('get-tiny-image')!; const result = await handler({}); expect(result.content).toHaveLength(3); expect(result.content[0]).toEqual({ - type: "text", + type: 'text', text: "Here's the image you requested:", }); expect(result.content[1]).toEqual({ - type: "image", + type: 'image', data: MCP_TINY_IMAGE, - mimeType: "image/png", + mimeType: 'image/png', }); expect(result.content[2]).toEqual({ - type: "text", - text: "The image above is the MCP logo.", + type: 'text', + text: 'The image above is the MCP logo.', }); }); - it("should return valid base64 image data", async () => { + it('should return valid base64 image data', async () => { const { mockServer, handlers } = createMockServer(); registerGetTinyImageTool(mockServer); - const handler = handlers.get("get-tiny-image")!; + const handler = handlers.get('get-tiny-image')!; const result = await handler({}); const imageContent = result.content[1]; - expect(imageContent.type).toBe("image"); - expect(imageContent.mimeType).toBe("image/png"); + expect(imageContent.type).toBe('image'); + expect(imageContent.mimeType).toBe('image/png'); // Verify it's valid base64 - expect(() => Buffer.from(imageContent.data, "base64")).not.toThrow(); + expect(() => Buffer.from(imageContent.data, 'base64')).not.toThrow(); }); }); - describe("get-structured-content", () => { - it("should return weather for New York", async () => { + describe('get-structured-content', () => { + it('should return weather for New York', async () => { const { mockServer, handlers } = createMockServer(); registerGetStructuredContentTool(mockServer); - const handler = handlers.get("get-structured-content")!; - const result = await handler({ location: "New York" }); + const handler = handlers.get('get-structured-content')!; + const result = await handler({ location: 'New York' }); expect(result.structuredContent).toEqual({ temperature: 33, - conditions: "Cloudy", + conditions: 'Cloudy', humidity: 82, }); - expect(result.content[0].type).toBe("text"); - expect(JSON.parse(result.content[0].text)).toEqual( - result.structuredContent - ); + expect(result.content[0].type).toBe('text'); + expect(JSON.parse(result.content[0].text)).toEqual(result.structuredContent); }); - it("should return weather for Chicago", async () => { + it('should return weather for Chicago', async () => { const { mockServer, handlers } = createMockServer(); registerGetStructuredContentTool(mockServer); - const handler = handlers.get("get-structured-content")!; - const result = await handler({ location: "Chicago" }); + const handler = handlers.get('get-structured-content')!; + const result = await handler({ location: 'Chicago' }); expect(result.structuredContent).toEqual({ temperature: 36, - conditions: "Light rain / drizzle", + conditions: 'Light rain / drizzle', humidity: 82, }); }); - it("should return weather for Los Angeles", async () => { + it('should return weather for Los Angeles', async () => { const { mockServer, handlers } = createMockServer(); registerGetStructuredContentTool(mockServer); - const handler = handlers.get("get-structured-content")!; - const result = await handler({ location: "Los Angeles" }); + const handler = handlers.get('get-structured-content')!; + const result = await handler({ location: 'Los Angeles' }); expect(result.structuredContent).toEqual({ temperature: 73, - conditions: "Sunny / Clear", + conditions: 'Sunny / Clear', humidity: 48, }); }); }); - describe("get-annotated-message", () => { - it("should return error message with high priority", async () => { + describe('get-annotated-message', () => { + it('should return error message with high priority', async () => { const { mockServer, handlers } = createMockServer(); registerGetAnnotatedMessageTool(mockServer); - const handler = handlers.get("get-annotated-message")!; - const result = await handler({ - messageType: "error", - includeImage: false, - }); + const handler = handlers.get('get-annotated-message')!; + const result = await handler({ messageType: 'error', includeImage: false }); expect(result.content).toHaveLength(1); - expect(result.content[0].text).toBe("Error: Operation failed"); + expect(result.content[0].text).toBe('Error: Operation failed'); expect(result.content[0].annotations).toEqual({ priority: 1.0, - audience: ["user", "assistant"], + audience: ['user', 'assistant'], }); }); - it("should return success message with medium priority", async () => { + it('should return success message with medium priority', async () => { const { mockServer, handlers } = createMockServer(); registerGetAnnotatedMessageTool(mockServer); - const handler = handlers.get("get-annotated-message")!; - const result = await handler({ - messageType: "success", - includeImage: false, - }); + const handler = handlers.get('get-annotated-message')!; + const result = await handler({ messageType: 'success', includeImage: false }); - expect(result.content[0].text).toBe("Operation completed successfully"); + expect(result.content[0].text).toBe('Operation completed successfully'); expect(result.content[0].annotations.priority).toBe(0.7); - expect(result.content[0].annotations.audience).toEqual(["user"]); + expect(result.content[0].annotations.audience).toEqual(['user']); }); - it("should return debug message with low priority", async () => { + it('should return debug message with low priority', async () => { const { mockServer, handlers } = createMockServer(); registerGetAnnotatedMessageTool(mockServer); - const handler = handlers.get("get-annotated-message")!; - const result = await handler({ - messageType: "debug", - includeImage: false, - }); + const handler = handlers.get('get-annotated-message')!; + const result = await handler({ messageType: 'debug', includeImage: false }); - expect(result.content[0].text).toContain("Debug:"); + expect(result.content[0].text).toContain('Debug:'); expect(result.content[0].annotations.priority).toBe(0.3); - expect(result.content[0].annotations.audience).toEqual(["assistant"]); + expect(result.content[0].annotations.audience).toEqual(['assistant']); }); - it("should include annotated image when requested", async () => { + it('should include annotated image when requested', async () => { const { mockServer, handlers } = createMockServer(); registerGetAnnotatedMessageTool(mockServer); - const handler = handlers.get("get-annotated-message")!; - const result = await handler({ - messageType: "success", - includeImage: true, - }); + const handler = handlers.get('get-annotated-message')!; + const result = await handler({ messageType: 'success', includeImage: true }); expect(result.content).toHaveLength(2); - expect(result.content[1].type).toBe("image"); + expect(result.content[1].type).toBe('image'); expect(result.content[1].annotations).toEqual({ priority: 0.5, - audience: ["user"], + audience: ['user'], }); }); }); - describe("trigger-long-running-operation", () => { - it("should complete operation and return result", async () => { + describe('trigger-long-running-operation', () => { + it('should complete operation and return result', async () => { const { mockServer, handlers } = createMockServer(); registerTriggerLongRunningOperationTool(mockServer); - const handler = handlers.get("trigger-long-running-operation")!; + const handler = handlers.get('trigger-long-running-operation')!; // Use very short duration for test const result = await handler( { duration: 0.1, steps: 2 }, - { mcpReq: { _meta: {}, id: "test-123" } } + { mcpReq: { _meta: {}, id: 'test-123' } } ); - expect(result.content[0].text).toContain( - "Long running operation completed" - ); - expect(result.content[0].text).toContain("Duration: 0.1 seconds"); - expect(result.content[0].text).toContain("Steps: 2"); + expect(result.content[0].text).toContain('Long running operation completed'); + expect(result.content[0].text).toContain('Duration: 0.1 seconds'); + expect(result.content[0].text).toContain('Steps: 2'); }, 10000); - it("should send progress notifications when progressToken provided", async () => { + it('should send progress notifications when progressToken provided', async () => { const { mockServer, handlers } = createMockServer(); registerTriggerLongRunningOperationTool(mockServer); - const handler = handlers.get("trigger-long-running-operation")!; + const handler = handlers.get('trigger-long-running-operation')!; await handler( { duration: 0.1, steps: 2 }, - { - mcpReq: { _meta: { progressToken: "token-123" }, id: "test-456" }, - sessionId: "session-1", - } + { mcpReq: { _meta: { progressToken: 'token-123' }, id: 'test-456' }, sessionId: 'session-1' } ); expect(mockServer.server.notification).toHaveBeenCalledTimes(2); expect(mockServer.server.notification).toHaveBeenCalledWith( expect.objectContaining({ - method: "notifications/progress", + method: 'notifications/progress', params: expect.objectContaining({ - progressToken: "token-123", + progressToken: 'token-123', }), }), expect.any(Object) @@ -386,46 +364,46 @@ describe("Tools", () => { }, 10000); }); - describe("get-resource-links", () => { - it("should return specified number of resource links", async () => { + describe('get-resource-links', () => { + it('should return specified number of resource links', async () => { const { mockServer, handlers } = createMockServer(); registerGetResourceLinksTool(mockServer); - const handler = handlers.get("get-resource-links")!; + const handler = handlers.get('get-resource-links')!; const result = await handler({ count: 3 }); // 1 intro text + 3 resource links expect(result.content).toHaveLength(4); - expect(result.content[0].type).toBe("text"); - expect(result.content[0].text).toContain("3 resource links"); + expect(result.content[0].type).toBe('text'); + expect(result.content[0].text).toContain('3 resource links'); // Check resource links for (let i = 1; i < 4; i++) { - expect(result.content[i].type).toBe("resource_link"); + expect(result.content[i].type).toBe('resource_link'); expect(result.content[i].uri).toBeDefined(); expect(result.content[i].name).toBeDefined(); } }); - it("should alternate between text and blob resources", async () => { + it('should alternate between text and blob resources', async () => { const { mockServer, handlers } = createMockServer(); registerGetResourceLinksTool(mockServer); - const handler = handlers.get("get-resource-links")!; + const handler = handlers.get('get-resource-links')!; const result = await handler({ count: 4 }); // Odd IDs (1, 3) are blob, even IDs (2, 4) are text - expect(result.content[1].name).toContain("Blob"); - expect(result.content[2].name).toContain("Text"); - expect(result.content[3].name).toContain("Blob"); - expect(result.content[4].name).toContain("Text"); + expect(result.content[1].name).toContain('Blob'); + expect(result.content[2].name).toContain('Text'); + expect(result.content[3].name).toContain('Blob'); + expect(result.content[4].name).toContain('Text'); }); - it("should use default count of 3", async () => { + it('should use default count of 3', async () => { const { mockServer, handlers } = createMockServer(); registerGetResourceLinksTool(mockServer); - const handler = handlers.get("get-resource-links")!; + const handler = handlers.get('get-resource-links')!; const result = await handler({}); // 1 intro text + 3 resource links (default) @@ -433,128 +411,128 @@ describe("Tools", () => { }); }); - describe("get-resource-reference", () => { - it("should return text resource reference", async () => { + describe('get-resource-reference', () => { + it('should return text resource reference', async () => { const { mockServer, handlers } = createMockServer(); registerGetResourceReferenceTool(mockServer); - const handler = handlers.get("get-resource-reference")!; - const result = await handler({ resourceType: "Text", resourceId: 1 }); + const handler = handlers.get('get-resource-reference')!; + const result = await handler({ resourceType: 'Text', resourceId: 1 }); expect(result.content).toHaveLength(3); - expect(result.content[0].text).toContain("Resource 1"); - expect(result.content[1].type).toBe("resource"); - expect(result.content[1].resource.uri).toContain("text/1"); - expect(result.content[2].text).toContain("URI"); + expect(result.content[0].text).toContain('Resource 1'); + expect(result.content[1].type).toBe('resource'); + expect(result.content[1].resource.uri).toContain('text/1'); + expect(result.content[2].text).toContain('URI'); }); - it("should return blob resource reference", async () => { + it('should return blob resource reference', async () => { const { mockServer, handlers } = createMockServer(); registerGetResourceReferenceTool(mockServer); - const handler = handlers.get("get-resource-reference")!; - const result = await handler({ resourceType: "Blob", resourceId: 5 }); + const handler = handlers.get('get-resource-reference')!; + const result = await handler({ resourceType: 'Blob', resourceId: 5 }); - expect(result.content[1].resource.uri).toContain("blob/5"); + expect(result.content[1].resource.uri).toContain('blob/5'); }); - it("should reject invalid resource type", async () => { + it('should reject invalid resource type', async () => { const { mockServer, handlers } = createMockServer(); registerGetResourceReferenceTool(mockServer); - const handler = handlers.get("get-resource-reference")!; - await expect( - handler({ resourceType: "Invalid", resourceId: 1 }) - ).rejects.toThrow("Invalid resourceType"); + const handler = handlers.get('get-resource-reference')!; + await expect(handler({ resourceType: 'Invalid', resourceId: 1 })).rejects.toThrow( + 'Invalid resourceType' + ); }); - it("should reject invalid resource ID", async () => { + it('should reject invalid resource ID', async () => { const { mockServer, handlers } = createMockServer(); registerGetResourceReferenceTool(mockServer); - const handler = handlers.get("get-resource-reference")!; - await expect( - handler({ resourceType: "Text", resourceId: -1 }) - ).rejects.toThrow("Invalid resourceId"); - await expect( - handler({ resourceType: "Text", resourceId: 0 }) - ).rejects.toThrow("Invalid resourceId"); - await expect( - handler({ resourceType: "Text", resourceId: 1.5 }) - ).rejects.toThrow("Invalid resourceId"); + const handler = handlers.get('get-resource-reference')!; + await expect(handler({ resourceType: 'Text', resourceId: -1 })).rejects.toThrow( + 'Invalid resourceId' + ); + await expect(handler({ resourceType: 'Text', resourceId: 0 })).rejects.toThrow( + 'Invalid resourceId' + ); + await expect(handler({ resourceType: 'Text', resourceId: 1.5 })).rejects.toThrow( + 'Invalid resourceId' + ); }); }); - describe("toggle-simulated-logging", () => { - it("should start logging when not active", async () => { + describe('toggle-simulated-logging', () => { + it('should start logging when not active', async () => { const { mockServer, handlers } = createMockServer(); registerToggleSimulatedLoggingTool(mockServer); - const handler = handlers.get("toggle-simulated-logging")!; - const result = await handler({}, { sessionId: "test-session-1" }); + const handler = handlers.get('toggle-simulated-logging')!; + const result = await handler({}, { sessionId: 'test-session-1' }); - expect(result.content[0].text).toContain("Started"); - expect(result.content[0].text).toContain("test-session-1"); + expect(result.content[0].text).toContain('Started'); + expect(result.content[0].text).toContain('test-session-1'); }); - it("should stop logging when already active", async () => { + it('should stop logging when already active', async () => { const { mockServer, handlers } = createMockServer(); registerToggleSimulatedLoggingTool(mockServer); - const handler = handlers.get("toggle-simulated-logging")!; + const handler = handlers.get('toggle-simulated-logging')!; // First call starts logging - await handler({}, { sessionId: "test-session-2" }); + await handler({}, { sessionId: 'test-session-2' }); // Second call stops logging - const result = await handler({}, { sessionId: "test-session-2" }); + const result = await handler({}, { sessionId: 'test-session-2' }); - expect(result.content[0].text).toContain("Stopped"); - expect(result.content[0].text).toContain("test-session-2"); + expect(result.content[0].text).toContain('Stopped'); + expect(result.content[0].text).toContain('test-session-2'); }); - it("should handle undefined sessionId", async () => { + it('should handle undefined sessionId', async () => { const { mockServer, handlers } = createMockServer(); registerToggleSimulatedLoggingTool(mockServer); - const handler = handlers.get("toggle-simulated-logging")!; + const handler = handlers.get('toggle-simulated-logging')!; const result = await handler({}, {}); - expect(result.content[0].text).toContain("Started"); + expect(result.content[0].text).toContain('Started'); }); }); - describe("toggle-subscriber-updates", () => { - it("should start updates when not active", async () => { + describe('toggle-subscriber-updates', () => { + it('should start updates when not active', async () => { const { mockServer, handlers } = createMockServer(); registerToggleSubscriberUpdatesTool(mockServer); - const handler = handlers.get("toggle-subscriber-updates")!; - const result = await handler({}, { sessionId: "sub-session-1" }); + const handler = handlers.get('toggle-subscriber-updates')!; + const result = await handler({}, { sessionId: 'sub-session-1' }); - expect(result.content[0].text).toContain("Started"); - expect(result.content[0].text).toContain("sub-session-1"); + expect(result.content[0].text).toContain('Started'); + expect(result.content[0].text).toContain('sub-session-1'); }); - it("should stop updates when already active", async () => { + it('should stop updates when already active', async () => { const { mockServer, handlers } = createMockServer(); registerToggleSubscriberUpdatesTool(mockServer); - const handler = handlers.get("toggle-subscriber-updates")!; + const handler = handlers.get('toggle-subscriber-updates')!; // First call starts updates - await handler({}, { sessionId: "sub-session-2" }); + await handler({}, { sessionId: 'sub-session-2' }); // Second call stops updates - const result = await handler({}, { sessionId: "sub-session-2" }); + const result = await handler({}, { sessionId: 'sub-session-2' }); - expect(result.content[0].text).toContain("Stopped"); - expect(result.content[0].text).toContain("sub-session-2"); + expect(result.content[0].text).toContain('Stopped'); + expect(result.content[0].text).toContain('sub-session-2'); }); }); - describe("trigger-sampling-request", () => { - it("should not register when client does not support sampling", () => { + describe('trigger-sampling-request', () => { + it('should not register when client does not support sampling', () => { const { mockServer } = createMockServer(); registerTriggerSamplingRequestTool(mockServer); @@ -562,7 +540,7 @@ describe("Tools", () => { expect(mockServer.registerTool).not.toHaveBeenCalled(); }); - it("should register when client supports sampling", () => { + it('should register when client supports sampling', () => { const handlers: Map = new Map(); const mockServer = { registerTool: vi.fn((name: string, config: any, handler: Function) => { @@ -576,20 +554,20 @@ describe("Tools", () => { registerTriggerSamplingRequestTool(mockServer); expect(mockServer.registerTool).toHaveBeenCalledWith( - "trigger-sampling-request", + 'trigger-sampling-request', expect.objectContaining({ - title: "Trigger Sampling Request Tool", - description: expect.stringContaining("Sampling"), + title: 'Trigger Sampling Request Tool', + description: expect.stringContaining('Sampling'), }), expect.any(Function) ); }); - it("should send sampling request and return result", async () => { + it('should send sampling request and return result', async () => { const handlers: Map = new Map(); const mockSendRequest = vi.fn().mockResolvedValue({ - model: "test-model", - content: { type: "text", text: "LLM response" }, + model: 'test-model', + content: { type: 'text', text: 'LLM response' }, }); const mockServer = { @@ -603,34 +581,34 @@ describe("Tools", () => { registerTriggerSamplingRequestTool(mockServer); - const handler = handlers.get("trigger-sampling-request")!; + const handler = handlers.get('trigger-sampling-request')!; const result = await handler( - { prompt: "Test prompt", maxTokens: 50 }, + { prompt: 'Test prompt', maxTokens: 50 }, { mcpReq: { send: mockSendRequest } } ); expect(mockSendRequest).toHaveBeenCalledWith( expect.objectContaining({ - method: "sampling/createMessage", + method: 'sampling/createMessage', params: expect.objectContaining({ maxTokens: 50, }), }), expect.anything() ); - expect(result.content[0].text).toContain("LLM sampling result"); + expect(result.content[0].text).toContain('LLM sampling result'); }); }); - describe("trigger-elicitation-request", () => { - it("should not register when client does not support elicitation", () => { + describe('trigger-elicitation-request', () => { + it('should not register when client does not support elicitation', () => { const { mockServer } = createMockServer(); registerTriggerElicitationRequestTool(mockServer); expect(mockServer.registerTool).not.toHaveBeenCalled(); }); - it("should register when client supports elicitation", () => { + it('should register when client supports elicitation', () => { const handlers: Map = new Map(); const mockServer = { registerTool: vi.fn((name: string, config: any, handler: Function) => { @@ -644,23 +622,23 @@ describe("Tools", () => { registerTriggerElicitationRequestTool(mockServer); expect(mockServer.registerTool).toHaveBeenCalledWith( - "trigger-elicitation-request", + 'trigger-elicitation-request', expect.objectContaining({ - title: "Trigger Elicitation Request Tool", - description: expect.stringContaining("Elicitation"), + title: 'Trigger Elicitation Request Tool', + description: expect.stringContaining('Elicitation'), }), expect.any(Function) ); }); - it("should handle accept action with user content", async () => { + it('should handle accept action with user content', async () => { const handlers: Map = new Map(); const mockSendRequest = vi.fn().mockResolvedValue({ - action: "accept", + action: 'accept', content: { - name: "John Doe", + name: 'John Doe', check: true, - email: "john@example.com", + email: 'john@example.com', }, }); @@ -675,18 +653,18 @@ describe("Tools", () => { registerTriggerElicitationRequestTool(mockServer); - const handler = handlers.get("trigger-elicitation-request")!; + const handler = handlers.get('trigger-elicitation-request')!; const result = await handler({}, { mcpReq: { send: mockSendRequest } }); - expect(result.content[0].text).toContain("✅"); - expect(result.content[0].text).toContain("provided"); - expect(result.content[1].text).toContain("John Doe"); + expect(result.content[0].text).toContain('✅'); + expect(result.content[0].text).toContain('provided'); + expect(result.content[1].text).toContain('John Doe'); }); - it("should handle decline action", async () => { + it('should handle decline action', async () => { const handlers: Map = new Map(); const mockSendRequest = vi.fn().mockResolvedValue({ - action: "decline", + action: 'decline', }); const mockServer = { @@ -700,17 +678,17 @@ describe("Tools", () => { registerTriggerElicitationRequestTool(mockServer); - const handler = handlers.get("trigger-elicitation-request")!; + const handler = handlers.get('trigger-elicitation-request')!; const result = await handler({}, { mcpReq: { send: mockSendRequest } }); - expect(result.content[0].text).toContain("❌"); - expect(result.content[0].text).toContain("declined"); + expect(result.content[0].text).toContain('❌'); + expect(result.content[0].text).toContain('declined'); }); - it("should handle cancel action", async () => { + it('should handle cancel action', async () => { const handlers: Map = new Map(); const mockSendRequest = vi.fn().mockResolvedValue({ - action: "cancel", + action: 'cancel', }); const mockServer = { @@ -724,22 +702,22 @@ describe("Tools", () => { registerTriggerElicitationRequestTool(mockServer); - const handler = handlers.get("trigger-elicitation-request")!; + const handler = handlers.get('trigger-elicitation-request')!; const result = await handler({}, { mcpReq: { send: mockSendRequest } }); - expect(result.content[0].text).toContain("⚠️"); - expect(result.content[0].text).toContain("cancelled"); + expect(result.content[0].text).toContain('⚠️'); + expect(result.content[0].text).toContain('cancelled'); }); }); - describe("trigger-url-elicitation", () => { + describe('trigger-url-elicitation', () => { // The error-path marker is module-level state shared across cases; reset it // so tests are independent of order and of each other's leftover keys. beforeEach(() => { __resetIssuedErrorPathElicitations(); }); - it("should not register when client does not support URL elicitation", () => { + it('should not register when client does not support URL elicitation', () => { const handlers: Map = new Map(); const mockServer = { registerTool: vi.fn((name: string, config: any, handler: Function) => { @@ -755,7 +733,7 @@ describe("Tools", () => { expect(mockServer.registerTool).not.toHaveBeenCalled(); }); - it("should register when client supports URL elicitation", () => { + it('should register when client supports URL elicitation', () => { const handlers: Map = new Map(); const mockServer = { registerTool: vi.fn((name: string, config: any, handler: Function) => { @@ -769,19 +747,19 @@ describe("Tools", () => { registerTriggerUrlElicitationTool(mockServer); expect(mockServer.registerTool).toHaveBeenCalledWith( - "trigger-url-elicitation", + 'trigger-url-elicitation', expect.objectContaining({ - title: "Trigger URL Elicitation Tool", - description: expect.stringContaining("URL elicitation"), + title: 'Trigger URL Elicitation Tool', + description: expect.stringContaining('URL elicitation'), }), expect.any(Function) ); }); - it("should send URL-mode elicitation request when errorPath is false", async () => { + it('should send URL-mode elicitation request when errorPath is false', async () => { const handlers: Map = new Map(); const mockSendRequest = vi.fn().mockResolvedValue({ - action: "accept", + action: 'accept', }); const mockServer = { @@ -795,12 +773,12 @@ describe("Tools", () => { registerTriggerUrlElicitationTool(mockServer); - const handler = handlers.get("trigger-url-elicitation")!; + const handler = handlers.get('trigger-url-elicitation')!; const result = await handler( { - url: "https://example.com/verify", - message: "Open this page to verify your identity", - elicitationId: "elicitation-123", + url: 'https://example.com/verify', + message: 'Open this page to verify your identity', + elicitationId: 'elicitation-123', errorPath: false, }, { mcpReq: { send: mockSendRequest } } @@ -808,12 +786,12 @@ describe("Tools", () => { expect(mockSendRequest).toHaveBeenCalledWith( expect.objectContaining({ - method: "elicitation/create", + method: 'elicitation/create', params: expect.objectContaining({ - mode: "url", - url: "https://example.com/verify", - message: "Open this page to verify your identity", - elicitationId: "elicitation-123", + mode: 'url', + url: 'https://example.com/verify', + message: 'Open this page to verify your identity', + elicitationId: 'elicitation-123', }), }), expect.anything(), @@ -821,11 +799,11 @@ describe("Tools", () => { ); expect(result.content[0].text).toContain( - "✅ User completed the URL elicitation flow." + '✅ User completed the URL elicitation flow.' ); }); - it("should not register when client has no elicitation capability at all", () => { + it('should not register when client has no elicitation capability at all', () => { const mockServer = { registerTool: vi.fn(), server: { @@ -838,7 +816,7 @@ describe("Tools", () => { expect(mockServer.registerTool).not.toHaveBeenCalled(); }); - it("should not register when client capabilities are undefined", () => { + it('should not register when client capabilities are undefined', () => { const mockServer = { registerTool: vi.fn(), server: { @@ -851,10 +829,10 @@ describe("Tools", () => { expect(mockServer.registerTool).not.toHaveBeenCalled(); }); - it("should default the elicitationId to a random UUID when omitted", async () => { + it('should default the elicitationId to a random UUID when omitted', async () => { const handlers: Map = new Map(); const mockSendRequest = vi.fn().mockResolvedValue({ - action: "accept", + action: 'accept', }); const mockServer = { @@ -868,11 +846,11 @@ describe("Tools", () => { registerTriggerUrlElicitationTool(mockServer); - const handler = handlers.get("trigger-url-elicitation")!; + const handler = handlers.get('trigger-url-elicitation')!; await handler( { - url: "https://example.com/verify", - message: "Open this page to verify your identity", + url: 'https://example.com/verify', + message: 'Open this page to verify your identity', errorPath: false, }, { mcpReq: { send: mockSendRequest } } @@ -884,9 +862,9 @@ describe("Tools", () => { ); }); - it("should report a declined URL elicitation", async () => { + it('should report a declined URL elicitation', async () => { const handlers: Map = new Map(); - const mockSendRequest = vi.fn().mockResolvedValue({ action: "decline" }); + const mockSendRequest = vi.fn().mockResolvedValue({ action: 'decline' }); const mockServer = { registerTool: vi.fn((name: string, config: any, handler: Function) => { @@ -899,25 +877,23 @@ describe("Tools", () => { registerTriggerUrlElicitationTool(mockServer); - const handler = handlers.get("trigger-url-elicitation")!; + const handler = handlers.get('trigger-url-elicitation')!; const result = await handler( { - url: "https://example.com/verify", - message: "Open this page to verify your identity", - elicitationId: "elicitation-123", + url: 'https://example.com/verify', + message: 'Open this page to verify your identity', + elicitationId: 'elicitation-123', errorPath: false, }, { mcpReq: { send: mockSendRequest } } ); - expect(result.content[0].text).toContain( - "❌ User declined to open the URL" - ); + expect(result.content[0].text).toContain('❌ User declined to open the URL'); }); - it("should report a cancelled URL elicitation", async () => { + it('should report a cancelled URL elicitation', async () => { const handlers: Map = new Map(); - const mockSendRequest = vi.fn().mockResolvedValue({ action: "cancel" }); + const mockSendRequest = vi.fn().mockResolvedValue({ action: 'cancel' }); const mockServer = { registerTool: vi.fn((name: string, config: any, handler: Function) => { @@ -930,23 +906,23 @@ describe("Tools", () => { registerTriggerUrlElicitationTool(mockServer); - const handler = handlers.get("trigger-url-elicitation")!; + const handler = handlers.get('trigger-url-elicitation')!; const result = await handler( { - url: "https://example.com/verify", - message: "Open this page to verify your identity", - elicitationId: "elicitation-123", + url: 'https://example.com/verify', + message: 'Open this page to verify your identity', + elicitationId: 'elicitation-123', errorPath: false, }, { mcpReq: { send: mockSendRequest } } ); expect(result.content[0].text).toContain( - "⚠️ User cancelled the URL elicitation" + '⚠️ User cancelled the URL elicitation' ); }); - it("should throw MCP error -32042 with a prerequisite elicitation pointing at a different URL when errorPath is true", async () => { + it('should throw MCP error -32042 with a prerequisite elicitation pointing at a different URL when errorPath is true', async () => { const handlers: Map = new Map(); const mockServer = { registerTool: vi.fn((name: string, config: any, handler: Function) => { @@ -959,16 +935,16 @@ describe("Tools", () => { registerTriggerUrlElicitationTool(mockServer); - const handler = handlers.get("trigger-url-elicitation")!; + const handler = handlers.get('trigger-url-elicitation')!; expect.assertions(5); try { await handler( { - url: "https://example.com/connect", - message: "Authorization is required to continue.", - elicitationId: "elicitation-xyz", + url: 'https://example.com/connect', + message: 'Authorization is required to continue.', + elicitationId: 'elicitation-xyz', errorPath: true, }, {} @@ -976,19 +952,19 @@ describe("Tools", () => { } catch (error: any) { expect(error.code).toBe(-32042); const prerequisite = error.data.elicitations[0]; - expect(prerequisite.mode).toBe("url"); + expect(prerequisite.mode).toBe('url'); // The prerequisite must NOT reuse the failing URL, otherwise the client // would complete it, retry, hit the same error, and loop forever. - expect(prerequisite.url).toBe("https://modelcontextprotocol.io"); - expect(prerequisite.url).not.toBe("https://example.com/connect"); + expect(prerequisite.url).toBe('https://modelcontextprotocol.io'); + expect(prerequisite.url).not.toBe('https://example.com/connect'); // It carries its own elicitation id for the prerequisite itself. - expect(typeof prerequisite.elicitationId).toBe("string"); + expect(typeof prerequisite.elicitationId).toBe('string'); } }); - it("should ignore errorPath and take the request path when the same call is retried after the prerequisite", async () => { + it('should ignore errorPath and take the request path when the same call is retried after the prerequisite', async () => { const handlers: Map = new Map(); - const mockSendRequest = vi.fn().mockResolvedValue({ action: "accept" }); + const mockSendRequest = vi.fn().mockResolvedValue({ action: 'accept' }); const mockServer = { registerTool: vi.fn((name: string, config: any, handler: Function) => { @@ -1001,30 +977,27 @@ describe("Tools", () => { registerTriggerUrlElicitationTool(mockServer); - const handler = handlers.get("trigger-url-elicitation")!; + const handler = handlers.get('trigger-url-elicitation')!; // A real client retries with the *same* arguments and does not echo the // prerequisite's elicitationId. Note these args omit elicitationId, so the // correlation must rely on stable inputs (session + url), not a per-call // random id. const args = { - url: "https://example.com/connect", - message: "Authorization is required to continue.", + url: 'https://example.com/connect', + message: 'Authorization is required to continue.', errorPath: true, }; - const extra = { - sessionId: "session-1", - mcpReq: { send: mockSendRequest }, - }; + const extra = { sessionId: 'session-1', mcpReq: { send: mockSendRequest } }; // First call: error path issues the prerequisite and throws -32042. let prerequisiteUrl: string | undefined; try { await handler(args, extra); - throw new Error("expected first call to throw"); + throw new Error('expected first call to throw'); } catch (error: any) { expect(error.code).toBe(-32042); prerequisiteUrl = error.data.elicitations[0].url; - expect(prerequisiteUrl).toBe("https://modelcontextprotocol.io"); + expect(prerequisiteUrl).toBe('https://modelcontextprotocol.io'); expect(mockSendRequest).not.toHaveBeenCalled(); } @@ -1034,30 +1007,30 @@ describe("Tools", () => { expect(mockSendRequest).toHaveBeenCalledWith( expect.objectContaining({ - method: "elicitation/create", + method: 'elicitation/create', params: expect.objectContaining({ - mode: "url", - url: "https://example.com/connect", + mode: 'url', + url: 'https://example.com/connect', }), }), expect.anything(), expect.anything() ); expect(result.content[0].text).toContain( - "✅ User completed the URL elicitation flow." + '✅ User completed the URL elicitation flow.' ); }); }); - describe("get-roots-list", () => { - it("should not register when client does not support roots", () => { + describe('get-roots-list', () => { + it('should not register when client does not support roots', () => { const { mockServer } = createMockServer(); registerGetRootsListTool(mockServer); expect(mockServer.registerTool).not.toHaveBeenCalled(); }); - it("should register when client supports roots", () => { + it('should register when client supports roots', () => { const handlers: Map = new Map(); const mockServer = { registerTool: vi.fn((name: string, config: any, handler: Function) => { @@ -1071,18 +1044,18 @@ describe("Tools", () => { registerGetRootsListTool(mockServer); expect(mockServer.registerTool).toHaveBeenCalledWith( - "get-roots-list", + 'get-roots-list', expect.objectContaining({ - title: "Get Roots List Tool", - description: expect.stringContaining("roots"), + title: 'Get Roots List Tool', + description: expect.stringContaining('roots'), }), expect.any(Function) ); }); }); - describe("gzip-file-as-resource", () => { - it("should compress data URI and return resource link", async () => { + describe('gzip-file-as-resource', () => { + it('should compress data URI and return resource link', async () => { const registeredResources: any[] = []; const mockServer = { registerTool: vi.fn(), @@ -1102,22 +1075,18 @@ describe("Tools", () => { registerGZipFileAsResourceTool(mockServer); // Create a data URI with test content - const testContent = "Hello, World!"; - const dataUri = `data:text/plain;base64,${Buffer.from( - testContent - ).toString("base64")}`; - - const result = await handler!({ - name: "test.txt.gz", - data: dataUri, - outputType: "resourceLink", - }); + const testContent = 'Hello, World!'; + const dataUri = `data:text/plain;base64,${Buffer.from(testContent).toString('base64')}`; + + const result = await handler!( + { name: 'test.txt.gz', data: dataUri, outputType: 'resourceLink' } + ); - expect(result.content[0].type).toBe("resource_link"); - expect(result.content[0].uri).toContain("test.txt.gz"); + expect(result.content[0].type).toBe('resource_link'); + expect(result.content[0].uri).toContain('test.txt.gz'); }); - it("should return resource directly when outputType is resource", async () => { + it('should return resource directly when outputType is resource', async () => { const mockServer = { registerTool: vi.fn(), registerResource: vi.fn(), @@ -1132,23 +1101,19 @@ describe("Tools", () => { registerGZipFileAsResourceTool(mockServer); - const testContent = "Test content for compression"; - const dataUri = `data:text/plain;base64,${Buffer.from( - testContent - ).toString("base64")}`; + const testContent = 'Test content for compression'; + const dataUri = `data:text/plain;base64,${Buffer.from(testContent).toString('base64')}`; - const result = await handler!({ - name: "output.gz", - data: dataUri, - outputType: "resource", - }); + const result = await handler!( + { name: 'output.gz', data: dataUri, outputType: 'resource' } + ); - expect(result.content[0].type).toBe("resource"); - expect(result.content[0].resource.mimeType).toBe("application/gzip"); + expect(result.content[0].type).toBe('resource'); + expect(result.content[0].resource.mimeType).toBe('application/gzip'); expect(result.content[0].resource.blob).toBeDefined(); }); - it("should reject unsupported URL protocols", async () => { + it('should reject unsupported URL protocols', async () => { const mockServer = { registerTool: vi.fn(), registerResource: vi.fn(), @@ -1164,12 +1129,8 @@ describe("Tools", () => { registerGZipFileAsResourceTool(mockServer); await expect( - handler!({ - name: "test.gz", - data: "ftp://example.com/file.txt", - outputType: "resource", - }) - ).rejects.toThrow("Unsupported URL protocol"); + handler!({ name: 'test.gz', data: 'ftp://example.com/file.txt', outputType: 'resource' }) + ).rejects.toThrow('Unsupported URL protocol'); }); }); }); diff --git a/src/everything/resources/session.ts b/src/everything/resources/session.ts index 9e8c33dac5..f34ee2f7a4 100644 --- a/src/everything/resources/session.ts +++ b/src/everything/resources/session.ts @@ -1,9 +1,4 @@ -import { - McpServer, - RegisteredResource, - Resource, - ResourceLink, -} from "@modelcontextprotocol/server"; +import { McpServer, RegisteredResource, Resource, ResourceLink } from "@modelcontextprotocol/server"; /** * Tracks registered session resources by URI to allow updating/removing on re-registration. diff --git a/src/everything/tools/get-annotated-message.ts b/src/everything/tools/get-annotated-message.ts index d1d26dbef0..ba47d4e8a1 100644 --- a/src/everything/tools/get-annotated-message.ts +++ b/src/everything/tools/get-annotated-message.ts @@ -21,10 +21,10 @@ const config = { "Demonstrates how annotations can be used to provide metadata about content.", inputSchema: GetAnnotatedMessageSchema, annotations: { - readOnlyHint: true, // This tool only returns data, no side effects - destructiveHint: false, // Does not delete or modify anything - idempotentHint: true, // Same input always produces same output - openWorldHint: false, // Does not interact with external systems + readOnlyHint: true, // This tool only returns data, no side effects + destructiveHint: false, // Does not delete or modify anything + idempotentHint: true, // Same input always produces same output + openWorldHint: false, // Does not interact with external systems }, }; diff --git a/src/everything/tools/trigger-url-elicitation.ts b/src/everything/tools/trigger-url-elicitation.ts index 15e4851e3f..e685afeda9 100644 --- a/src/everything/tools/trigger-url-elicitation.ts +++ b/src/everything/tools/trigger-url-elicitation.ts @@ -1,11 +1,6 @@ import { randomUUID } from "node:crypto"; import { ElicitResultSchema } from "@modelcontextprotocol/core"; -import { - McpServer, - CallToolResult, - ElicitRequestURLParams, - UrlElicitationRequiredError, -} from "@modelcontextprotocol/server"; +import { McpServer, CallToolResult, ElicitRequestURLParams, UrlElicitationRequiredError } from "@modelcontextprotocol/server"; import { z } from "zod"; // Tool input schema @@ -125,9 +120,7 @@ export const registerTriggerUrlElicitationTool = (server: McpServer) => { // the session, the requested URL, and the caller-supplied elicitationId // (if any). Keying on the resolved/random elicitationId would change on // every call and never match, re-throwing the prerequisite forever. - const errorPathKey = `${sessionId}\u0000${url}\u0000${ - requestedElicitationId ?? "" - }`; + const errorPathKey = `${sessionId}\u0000${url}\u0000${requestedElicitationId ?? ""}`; const elicitationParams: ElicitRequestURLParams = { mode: "url", diff --git a/src/everything/vitest.config.ts b/src/everything/vitest.config.ts index e3149b5492..d414ec8f52 100644 --- a/src/everything/vitest.config.ts +++ b/src/everything/vitest.config.ts @@ -1,14 +1,14 @@ -import { defineConfig } from "vitest/config"; +import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { globals: true, - environment: "node", - include: ["**/__tests__/**/*.test.ts"], + environment: 'node', + include: ['**/__tests__/**/*.test.ts'], coverage: { - provider: "v8", - include: ["**/*.ts"], - exclude: ["**/__tests__/**", "**/dist/**"], + provider: 'v8', + include: ['**/*.ts'], + exclude: ['**/__tests__/**', '**/dist/**'], }, }, }); diff --git a/src/filesystem/__tests__/roots-utils.test.ts b/src/filesystem/__tests__/roots-utils.test.ts index fbdf45d6d6..39ef1b3089 100644 --- a/src/filesystem/__tests__/roots-utils.test.ts +++ b/src/filesystem/__tests__/roots-utils.test.ts @@ -3,7 +3,7 @@ import { getValidRootDirectories } from '../roots-utils.js'; import { mkdtempSync, rmSync, mkdirSync, writeFileSync, realpathSync } from 'fs'; import { tmpdir } from 'os'; import { join } from 'path'; -import type { Root } from "@modelcontextprotocol/server"; +import type { Root } from '@modelcontextprotocol/server'; describe('getValidRootDirectories', () => { let testDir1: string; diff --git a/src/filesystem/__tests__/structured-content.test.ts b/src/filesystem/__tests__/structured-content.test.ts index 64403ea842..362e619100 100644 --- a/src/filesystem/__tests__/structured-content.test.ts +++ b/src/filesystem/__tests__/structured-content.test.ts @@ -2,8 +2,8 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import * as fs from 'fs/promises'; import * as path from 'path'; import * as os from 'os'; -import { StdioClientTransport } from "@modelcontextprotocol/client/stdio"; -import { Client } from "@modelcontextprotocol/client"; +import { StdioClientTransport } from '@modelcontextprotocol/client/stdio'; +import { Client } from '@modelcontextprotocol/client'; import { spawn } from 'child_process'; /** diff --git a/src/filesystem/index.ts b/src/filesystem/index.ts index c500e8b3e0..ae9bb1e994 100644 --- a/src/filesystem/index.ts +++ b/src/filesystem/index.ts @@ -1,8 +1,8 @@ #!/usr/bin/env node -import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; -import { McpServer, CallToolResult } from "@modelcontextprotocol/server"; -import type { Root } from "@modelcontextprotocol/server"; +import { StdioServerTransport } from '@modelcontextprotocol/server/stdio'; +import { McpServer, CallToolResult } from '@modelcontextprotocol/server'; +import type { Root } from '@modelcontextprotocol/server'; import fs from "fs/promises"; import { createReadStream } from "fs"; import path from "path"; @@ -206,7 +206,6 @@ const readTextFileHandler = async (args: z.infer) }; }; -/* @mcp-codemod-error Could not verify `inputSchema` is a schema object. Raw shapes are deprecated in v2 — pass a Standard Schema object (e.g. z.object({ … })); no change is needed if it already is one. */ server.registerTool( "read_file", { @@ -232,10 +231,10 @@ server.registerTool( "the last N lines of a file. Operates on the file as text regardless of extension. " + "Only works within allowed directories.", inputSchema: z.object({ - path: z.string(), - tail: z.number().optional().describe("If provided, returns only the last N lines of the file"), - head: z.number().optional().describe("If provided, returns only the first N lines of the file") - }), + path: z.string(), + tail: z.number().optional().describe("If provided, returns only the last N lines of the file"), + head: z.number().optional().describe("If provided, returns only the first N lines of the file") + }), outputSchema: z.object({ content: z.string() }), annotations: { readOnlyHint: true } }, @@ -250,15 +249,15 @@ server.registerTool( "Read an image or audio file. Returns the base64 encoded data and MIME type. " + "Only works within allowed directories.", inputSchema: z.object({ - path: z.string() - }), + path: z.string() + }), outputSchema: z.object({ - content: z.array(z.object({ - type: z.enum(["image", "audio", "blob"]), - data: z.string(), - mimeType: z.string() - })) - }), + content: z.array(z.object({ + type: z.enum(["image", "audio", "blob"]), + data: z.string(), + mimeType: z.string() + })) + }), annotations: { readOnlyHint: true } }, async (args: z.infer) => { @@ -305,10 +304,10 @@ server.registerTool( "path as a reference. Failed reads for individual files won't stop " + "the entire operation. Only works within allowed directories.", inputSchema: z.object({ - paths: z.array(z.string()) - .min(1) - .describe("Array of file paths to read. Each path must be a string pointing to a valid file within allowed directories.") - }), + paths: z.array(z.string()) + .min(1) + .describe("Array of file paths to read. Each path must be a string pointing to a valid file within allowed directories.") + }), outputSchema: z.object({ content: z.string() }), annotations: { readOnlyHint: true } }, @@ -342,9 +341,9 @@ server.registerTool( "Use with caution as it will overwrite existing files without warning. " + "Handles text content with proper encoding. Only works within allowed directories.", inputSchema: z.object({ - path: z.string(), - content: z.string() - }), + path: z.string(), + content: z.string() + }), outputSchema: z.object({ content: z.string() }), annotations: { readOnlyHint: false, idempotentHint: true, destructiveHint: true } }, @@ -368,13 +367,13 @@ server.registerTool( "with new content. Returns a git-style diff showing the changes made. " + "Only works within allowed directories.", inputSchema: z.object({ - path: z.string(), - edits: z.array(z.object({ - oldText: z.string().describe("Text to search for - must match exactly"), - newText: z.string().describe("Text to replace with") - })), - dryRun: z.boolean().default(false).describe("Preview changes using git-style diff format") - }), + path: z.string(), + edits: z.array(z.object({ + oldText: z.string().describe("Text to search for - must match exactly"), + newText: z.string().describe("Text to replace with") + })), + dryRun: z.boolean().default(false).describe("Preview changes using git-style diff format") + }), outputSchema: z.object({ content: z.string() }), annotations: { readOnlyHint: false, idempotentHint: false, destructiveHint: true } }, @@ -398,8 +397,8 @@ server.registerTool( "this operation will succeed silently. Perfect for setting up directory " + "structures for projects or ensuring required paths exist. Only works within allowed directories.", inputSchema: z.object({ - path: z.string() - }), + path: z.string() + }), outputSchema: z.object({ content: z.string() }), annotations: { readOnlyHint: false, idempotentHint: true, destructiveHint: false } }, @@ -424,8 +423,8 @@ server.registerTool( "prefixes. This tool is essential for understanding directory structure and " + "finding specific files within a directory. Only works within allowed directories.", inputSchema: z.object({ - path: z.string() - }), + path: z.string() + }), outputSchema: z.object({ content: z.string() }), annotations: { readOnlyHint: true } }, @@ -452,9 +451,9 @@ server.registerTool( "prefixes. This tool is useful for understanding directory structure and " + "finding specific files within a directory. Only works within allowed directories.", inputSchema: z.object({ - path: z.string(), - sortBy: z.enum(["name", "size"]).optional().default("name").describe("Sort entries by name or size") - }), + path: z.string(), + sortBy: z.enum(["name", "size"]).optional().default("name").describe("Sort entries by name or size") + }), outputSchema: z.object({ content: z.string() }), annotations: { readOnlyHint: true } }, @@ -531,9 +530,9 @@ server.registerTool( "Files have no children array, while directories always have a children array (which may be empty). " + "The output is formatted with 2-space indentation for readability. Only works within allowed directories.", inputSchema: z.object({ - path: z.string(), - excludePatterns: z.array(z.string()).optional().default([]) - }), + path: z.string(), + excludePatterns: z.array(z.string()).optional().default([]) + }), outputSchema: z.object({ content: z.string() }), annotations: { readOnlyHint: true } }, @@ -601,9 +600,9 @@ server.registerTool( "operation will fail. Works across different directories and can be used " + "for simple renaming within the same directory. Both source and destination must be within allowed directories.", inputSchema: z.object({ - source: z.string(), - destination: z.string() - }), + source: z.string(), + destination: z.string() + }), outputSchema: z.object({ content: z.string() }), annotations: { readOnlyHint: false, idempotentHint: false, destructiveHint: true } }, @@ -631,10 +630,10 @@ server.registerTool( "Returns full paths to all matching items. Great for finding files when you don't know their exact location. " + "Only searches within allowed directories.", inputSchema: z.object({ - path: z.string(), - pattern: z.string(), - excludePatterns: z.array(z.string()).optional().default([]) - }), + path: z.string(), + pattern: z.string(), + excludePatterns: z.array(z.string()).optional().default([]) + }), outputSchema: z.object({ content: z.string() }), annotations: { readOnlyHint: true } }, @@ -659,8 +658,8 @@ server.registerTool( "and type. This tool is perfect for understanding file characteristics " + "without reading the actual content. Only works within allowed directories.", inputSchema: z.object({ - path: z.string() - }), + path: z.string() + }), outputSchema: z.object({ content: z.string() }), annotations: { readOnlyHint: true } }, diff --git a/src/filesystem/roots-utils.ts b/src/filesystem/roots-utils.ts index 252b9400cf..39b05c733f 100644 --- a/src/filesystem/roots-utils.ts +++ b/src/filesystem/roots-utils.ts @@ -2,7 +2,7 @@ import { promises as fs, type Stats } from 'fs'; import path from 'path'; import os from 'os'; import { normalizePath } from './path-utils.js'; -import type { Root } from "@modelcontextprotocol/server"; +import type { Root } from '@modelcontextprotocol/server'; import { fileURLToPath } from "url"; /** diff --git a/src/sequentialthinking/index.ts b/src/sequentialthinking/index.ts index 7ba2b72fdf..15fbbd527f 100644 --- a/src/sequentialthinking/index.ts +++ b/src/sequentialthinking/index.ts @@ -1,7 +1,7 @@ #!/usr/bin/env node -import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; -import { McpServer } from "@modelcontextprotocol/server"; +import { StdioServerTransport } from '@modelcontextprotocol/server/stdio'; +import { McpServer } from '@modelcontextprotocol/server'; import { z } from "zod"; import { SequentialThinkingServer } from './lib.js'; @@ -81,16 +81,16 @@ You should: 10. Provide a single, ideally correct answer as the final output 11. Only set nextThoughtNeeded to false when truly done and a satisfactory answer is reached`, inputSchema: z.object({ - thought: z.string().describe("Your current thinking step"), - nextThoughtNeeded: coercedBoolean.describe("Whether another thought step is needed"), - thoughtNumber: z.coerce.number().int().min(1).describe("Current thought number (numeric value, e.g., 1, 2, 3)"), - totalThoughts: z.coerce.number().int().min(1).describe("Estimated total thoughts needed (numeric value, e.g., 5, 10)"), - isRevision: coercedBoolean.optional().describe("Whether this revises previous thinking"), - revisesThought: z.coerce.number().int().min(1).optional().describe("Which thought is being reconsidered"), - branchFromThought: z.coerce.number().int().min(1).optional().describe("Branching point thought number"), - branchId: z.string().optional().describe("Branch identifier"), - needsMoreThoughts: coercedBoolean.optional().describe("If more thoughts are needed") - }), + thought: z.string().describe("Your current thinking step"), + nextThoughtNeeded: coercedBoolean.describe("Whether another thought step is needed"), + thoughtNumber: z.coerce.number().int().min(1).describe("Current thought number (numeric value, e.g., 1, 2, 3)"), + totalThoughts: z.coerce.number().int().min(1).describe("Estimated total thoughts needed (numeric value, e.g., 5, 10)"), + isRevision: coercedBoolean.optional().describe("Whether this revises previous thinking"), + revisesThought: z.coerce.number().int().min(1).optional().describe("Which thought is being reconsidered"), + branchFromThought: z.coerce.number().int().min(1).optional().describe("Branching point thought number"), + branchId: z.string().optional().describe("Branch identifier"), + needsMoreThoughts: coercedBoolean.optional().describe("If more thoughts are needed") + }), annotations: { readOnlyHint: true, destructiveHint: false, @@ -98,12 +98,12 @@ You should: openWorldHint: false, }, outputSchema: z.object({ - thoughtNumber: z.number(), - totalThoughts: z.number(), - nextThoughtNeeded: z.boolean(), - branches: z.array(z.string()), - thoughtHistoryLength: z.number() - }), + thoughtNumber: z.number(), + totalThoughts: z.number(), + nextThoughtNeeded: z.boolean(), + branches: z.array(z.string()), + thoughtHistoryLength: z.number() + }), }, async (args) => { const result = thinkingServer.processThought(args);