From 8bfe65f88fcaadcfac64ddf415e2ff8e56260472 Mon Sep 17 00:00:00 2001 From: Matthew Podwysocki Date: Wed, 21 Oct 2026 15:26:56 -0400 Subject: [PATCH 1/4] feat!: move to MCP TypeScript SDK v2 Swaps @modelcontextprotocol/sdk 1.x for the v2 packages (@modelcontextprotocol/server and /core, plus /client for tests) and ext-apps 2.0.3, same as mapbox/mcp-server#296. Ran the official v1-to-v2 codemod, then: - Replace the SDK output-validation patch with lenientOutputSchema, a Standard Schema wrapper that keeps the real schema in tools/list but only warns on a mismatch. Tools that return text only get structuredContent {}. - Register prompt argument schemas as z.object(), since v2 deprecates raw shapes. - Move test request contexts to the v2 shape (http.authInfo) so the token checks actually run. - Add tests that make a real tools/call through McpServer. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 9 + CLAUDE.md | 2 +- docs/engineering_standards.md | 2 +- package-lock.json | 173 +++++++----------- package.json | 6 +- .../@modelcontextprotocol+sdk+1.30.0.patch | 55 ------ patches/README.md | 44 +---- src/index.ts | 6 +- src/prompts/AnalyzeGeojsonPrompt.ts | 2 +- src/prompts/BasePrompt.ts | 2 +- src/prompts/BuildCustomMapPrompt.ts | 2 +- src/prompts/CreateAndPreviewStylePrompt.ts | 2 +- src/prompts/DebugMapboxIntegrationPrompt.ts | 2 +- src/prompts/DesignDataDrivenStylePrompt.ts | 2 +- .../PrepareStyleForProductionPrompt.ts | 2 +- src/prompts/SetupMapboxProjectPrompt.ts | 2 +- src/resources/BaseResource.ts | 10 +- src/resources/resourceRegistry.ts | 4 +- src/resources/ui-apps/MapPreviewUIResource.ts | 10 +- .../ui-apps/StyleComparisonUIResource.ts | 8 +- src/tools/BaseTool.ts | 45 +++-- src/tools/MapboxApiBasedTool.ts | 18 +- .../bounding-box-tool/BoundingBoxTool.ts | 2 +- .../CountryBoundingBoxTool.ts | 2 +- .../CheckColorContrastTool.ts | 2 +- .../compare-styles-tool/CompareStylesTool.ts | 2 +- .../CoordinateConversionTool.ts | 2 +- .../create-style-tool/CreateStyleTool.ts | 2 +- .../create-token-tool/CreateTokenTool.ts | 2 +- .../delete-style-tool/DeleteStyleTool.ts | 2 +- .../GeojsonPreviewTool.ts | 2 +- .../get-feedback-tool/GetFeedbackTool.ts | 2 +- src/tools/lenientOutputSchema.ts | 41 +++++ .../list-feedback-tool/ListFeedbackTool.ts | 2 +- src/tools/list-styles-tool/ListStylesTool.ts | 2 +- src/tools/list-tokens-tool/ListTokensTool.ts | 2 +- .../optimize-style-tool/OptimizeStyleTool.ts | 2 +- .../preview-style-tool/PreviewStyleTool.ts | 2 +- .../retrieve-style-tool/RetrieveStyleTool.ts | 2 +- .../style-builder-tool/StyleBuilderTool.ts | 2 +- .../StyleComparisonTool.ts | 2 +- src/tools/tilequery-tool/TilequeryTool.ts | 2 +- .../update-style-tool/UpdateStyleTool.ts | 2 +- .../ValidateExpressionTool.ts | 2 +- .../ValidateGeojsonTool.ts | 2 +- .../validate-style-tool/ValidateStyleTool.ts | 2 +- test/integration/loggingOrder.test.ts | 7 +- test/resources/resourceRegistry.test.ts | 9 +- .../ui-apps/MapPreviewUIResource.test.ts | 2 +- test/security/path-traversal.test.ts | 6 +- test/tools/MapboxApiBasedTool.test.ts | 2 +- test/tools/lenientOutputSchema.test.ts | 130 +++++++++++++ 52 files changed, 355 insertions(+), 296 deletions(-) delete mode 100644 patches/@modelcontextprotocol+sdk+1.30.0.patch create mode 100644 src/tools/lenientOutputSchema.ts create mode 100644 test/tools/lenientOutputSchema.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 17d822e..e7cd591 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,15 @@ ### Breaking Changes +- **MCP TypeScript SDK v2 (PR number TBD; same move as mapbox/mcp-server#296).** The server and the `@mapbox/mcp-devkit-server/tools`, `/resources` and `/prompts` exports now use the v2 SDK packages (`@modelcontextprotocol/server` and `@modelcontextprotocol/core`, 2.3.1) instead of `@modelcontextprotocol/sdk` 1.x, and `@modelcontextprotocol/ext-apps` moves from 1.x to 2.0.3. v2 is where new MCP features land. + - **For code that imports our tools:** `installTo(server)` needs a v2 `McpServer`; a v1 server from `@modelcontextprotocol/sdk` won't work. The hosted server builds its own server, so it has to move to v2 in the same release as this and mcp-server. Tool and resource handlers now get the v2 context, so the access token is at `ctx.http.authInfo.token` instead of `extra.authInfo.token`. + - **Clients:** nothing changes. It still negotiates 2025-11-25 with current clients, and the MCP Apps wire protocol is the same in ext-apps 2.x, so the preview and comparison UIs work as before. + - **No more SDK patch:** the v1 SDK was patched so a tool's `structuredContent` that didn't match its output schema, or was missing, logged a warning instead of failing the call. v2 takes any Standard Schema as an output schema, so `lenientOutputSchema` wraps each tool's Zod schema: `tools/list` publishes the real JSON Schema (now 2020-12, from v2) and validation only warns. A tool with an output schema that returns only text gets `structuredContent: {}` instead of an error. `patches/` is now empty. + - **Prompts:** argument schemas are registered as a `z.object(...)`, since v2 deprecates raw Zod shapes there. + - **Codemod:** most of the change is `npx @modelcontextprotocol/codemod v1-to-v2 .` (import paths, `RequestHandlerExtra` to `ServerContext`, `extra.authInfo` to `ctx.http?.authInfo`). `@modelcontextprotocol/client` is a dev dependency, used only by tests. + - **Still on v1 transitively:** `@mcp-ui/server` (6.1.0, the latest) depends on `@modelcontextprotocol/sdk` 1.x, so a copy stays in `node_modules`. We only call its `createUIResource()`, which builds a plain content object, so nothing crosses between the two SDKs. + - **Testing:** new tests make a real `tools/call` through `McpServer` for mismatched, missing and matching `structuredContent`, and fail without the wrapper. Tests that built a request context by hand used the v1 `{ authInfo }` shape, so the token never reached the code under test (including the path-traversal checks on usernames from the token and the map preview's token checks); they now use `{ http: { authInfo } }`. + - **Consolidated `GeojsonPreviewUIResource` and `PreviewStyleUIResource` into a single `MapPreviewUIResource`.** Both were near-identical hand-written MCP Apps templates — the same postMessage handshake, fullscreen/open-link controls, and resize handling copy-pasted across ~650 lines, differing only in what they drew on the map once a tool result arrived (a GeoJSON overlay on the default Standard style vs. swapping to an arbitrary preview style). `geojson_preview_tool` and `preview_style_tool` now both declare `_meta.ui.resourceUri: 'ui://mapbox/map-preview/index.html'`, served by the merged resource, which dispatches on the shape of the tool-result URL it receives (a `geojson.io` URL vs. a Styles API `.html?access_token=...` preview URL) rather than assuming a fixed mode. Neither tool's own input/output contract changed. - The public resource exports change: `previewStyleUI`/`geojsonPreviewUI` (from `@mapbox/mcp-devkit-server/resources`) are replaced by a single `mapPreviewUI`. `GeojsonPreviewUIResource`/`PreviewStyleUIResource` class exports are removed in favor of `MapPreviewUIResource`. - `style_comparison_tool`'s `StyleComparisonUIResource` is untouched and stays separate — it renders two synced `mapboxgl.Map` instances under a swipe/compare slider, a genuinely different UI shape from "one map, different content," not just another mode to fold in. diff --git a/CLAUDE.md b/CLAUDE.md index b7d7228..a511458 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -33,7 +33,7 @@ The codebase organizes into: **Resource System:** Static reference data exposed as MCP resources using URI pattern `resource://mapbox-*`, including style layer specs, Streets v8 field definitions, and token scope documentation. -**Token Management:** Tools receive `MAPBOX_ACCESS_TOKEN` via `extra.authInfo.token` or environment variable. Token scope validation is critical—most tool failures stem from insufficient scopes (see `README.md` for per-tool requirements). +**Token Management:** Tools receive `MAPBOX_ACCESS_TOKEN` via `ctx.http.authInfo.token` (SDK v2 `ServerContext`) or environment variable. Token scope validation is critical—most tool failures stem from insufficient scopes (see `README.md` for per-tool requirements). **Tool Registry:** Tools are auto-discovered via `src/tools/toolRegistry.ts` exports. No manual registration required—just export from registry. diff --git a/docs/engineering_standards.md b/docs/engineering_standards.md index 64a34ae..9af8b15 100644 --- a/docs/engineering_standards.md +++ b/docs/engineering_standards.md @@ -72,7 +72,7 @@ This approach supports dependency injection and maintains testability. See `src/ All Mapbox API tools require `MAPBOX_ACCESS_TOKEN` with specific scopes: -- Tools receive tokens via `extra.authInfo.token` or `process.env.MAPBOX_ACCESS_TOKEN` +- Tools receive tokens via `ctx.http.authInfo.token` or `process.env.MAPBOX_ACCESS_TOKEN` - Document required scopes in `README.md` for each tool - Token scope mismatches are the primary failure mode - Use `VERBOSE_ERRORS=true` for debugging authentication issues diff --git a/package-lock.json b/package-lock.json index 6e5d27a..e2a7f33 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,8 +12,9 @@ "dependencies": { "@mapbox/mapbox-gl-style-spec": "^14.26.0", "@mcp-ui/server": "^6.1.0", - "@modelcontextprotocol/ext-apps": "^1.1.1", - "@modelcontextprotocol/sdk": "^1.30.0", + "@modelcontextprotocol/core": "^2.3.1", + "@modelcontextprotocol/ext-apps": "^2.0.3", + "@modelcontextprotocol/server": "^2.3.1", "@opentelemetry/api": "^1.9.1", "@opentelemetry/auto-instrumentations-node": "^0.74.0", "@opentelemetry/exporter-trace-otlp-http": "^0.216.0", @@ -29,6 +30,7 @@ }, "devDependencies": { "@eslint/js": "^9.27.0", + "@modelcontextprotocol/client": "^2.3.1", "@types/geojson": "^7946.0.16", "@types/node": "^22.0.0", "@typescript-eslint/eslint-plugin": "^8.58.2", @@ -1521,41 +1523,62 @@ "win32" ] }, + "node_modules/@modelcontextprotocol/client": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/client/-/client-2.3.1.tgz", + "integrity": "sha512-mIGZXpHsjnZ6lD+gD/WCMpR5k8yVQQ8nNFH1N0Srf7AvnwTUMYD6pvTY8ng2+He5FfV7YMZLmrjL7cLa/cc3dQ==", + "license": "Apache-2.0", + "dependencies": { + "@modelcontextprotocol/core": "2.3.1", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.8", + "jose": "^6.1.3", + "pkce-challenge": "^5.0.0", + "zod": "^4.2.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@modelcontextprotocol/core": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/core/-/core-2.3.1.tgz", + "integrity": "sha512-laVmIhPGpWi7rG9q+0UigblRN0Hws/Yjlb6qLFtVqryXVLkc4h+yN8z731POD2LnAGQlFe7/tXhUm7Bi0YB91g==", + "license": "Apache-2.0", + "dependencies": { + "zod": "^4.2.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/@modelcontextprotocol/ext-apps": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/ext-apps/-/ext-apps-1.1.1.tgz", - "integrity": "sha512-uK6ddMa/8vK7JaDQTIqa9Ybhwx1TCVey21MlM9/AQFQ3OSh2+LInWB0UzPrprF0Ah3s0dTe9eCDNZ42msCouiw==", - "hasInstallScript": true, + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/ext-apps/-/ext-apps-2.0.3.tgz", + "integrity": "sha512-Tn+4+cyhO4f4cQSDxNyLiIfEE+qlTTuIbWBJB8JTinEXOhDYgCT4wDTQplxD9MBYZH5pCYGC59GaxiwIT4bu8w==", "license": "MIT", "workspaces": [ "examples/*" ], - "optionalDependencies": { - "@oven/bun-darwin-aarch64": "^1.2.21", - "@oven/bun-darwin-x64": "^1.2.21", - "@oven/bun-darwin-x64-baseline": "^1.2.21", - "@oven/bun-linux-aarch64": "^1.2.21", - "@oven/bun-linux-aarch64-musl": "^1.2.21", - "@oven/bun-linux-x64": "^1.2.21", - "@oven/bun-linux-x64-baseline": "^1.2.21", - "@oven/bun-linux-x64-musl": "^1.2.21", - "@oven/bun-linux-x64-musl-baseline": "^1.2.21", - "@oven/bun-windows-x64": "^1.2.21", - "@oven/bun-windows-x64-baseline": "^1.2.21", - "@rollup/rollup-darwin-arm64": "^4.53.3", - "@rollup/rollup-darwin-x64": "^4.53.3", - "@rollup/rollup-linux-arm64-gnu": "^4.53.3", - "@rollup/rollup-linux-x64-gnu": "^4.53.3", - "@rollup/rollup-win32-arm64-msvc": "^4.53.3", - "@rollup/rollup-win32-x64-msvc": "^4.53.3" + "dependencies": { + "@standard-schema/spec": "^1.1.0" + }, + "engines": { + "node": ">=20" }, "peerDependencies": { - "@modelcontextprotocol/sdk": "^1.24.0", + "@modelcontextprotocol/client": "^2.0.0", + "@modelcontextprotocol/core": "^2.0.0", + "@modelcontextprotocol/server": "^2.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0", - "zod": "^3.25.0 || ^4.0.0" + "zod": "^4.2.0" }, "peerDependenciesMeta": { + "@modelcontextprotocol/server": { + "optional": true + }, "react": { "optional": true }, @@ -1564,84 +1587,6 @@ } } }, - "node_modules/@modelcontextprotocol/ext-apps/node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.0.tgz", - "integrity": "sha512-/NNIj9A7yLjKdmkx5dC2XQ9DmjIECpGpwHoGmA5E1AhU0fuICSqSWScPhN1yLCkEdkCwJIDu2xIeLPs60MNIVg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@modelcontextprotocol/ext-apps/node_modules/@rollup/rollup-darwin-x64": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.0.tgz", - "integrity": "sha512-xoh8abqgPrPYPr7pTYipqnUi1V3em56JzE/HgDgitTqZBZ3yKCWI+7KUkceM6tNweyUKYru1UMi7FC060RyKwA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@modelcontextprotocol/ext-apps/node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.0.tgz", - "integrity": "sha512-Xws2KA4CLvZmXjy46SQaXSejuKPhwVdaNinldoYfqruZBaJHqVo6hnRa8SDo9z7PBW5x84SH64+izmldCgbezw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@modelcontextprotocol/ext-apps/node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.0.tgz", - "integrity": "sha512-OR5p5yG5OKSxHReWmwvM0P+VTPMwoBS45PXTMYaskKQqybkS3Kmugq1W+YbNWArF8/s7jQScgzXUhArzEQ7x0A==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@modelcontextprotocol/ext-apps/node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.0.tgz", - "integrity": "sha512-XnaaaSMGSI6Wk8F4KK3QP7GfuuhjGchElsVerCplUuxRIzdvZ7hRBpLR0omCmw+kI2RFJB80nenhOoGXlJ5TfQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@modelcontextprotocol/ext-apps/node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.0.tgz", - "integrity": "sha512-Zv7v6q6aV+VslnpwzqKAmrk5JdVkLUzok2208ZXGipjb+msxBr/fJPZyeEXiFgH7k62Ak0SLIfxQRZQvTuf7rQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, "node_modules/@modelcontextprotocol/sdk": { "version": "1.30.0", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.30.0.tgz", @@ -1704,6 +1649,19 @@ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "license": "MIT" }, + "node_modules/@modelcontextprotocol/server": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/server/-/server-2.3.1.tgz", + "integrity": "sha512-e59MfWuSssj6DoQ96JCTqdW1AXT5/XtnTl0WWd6MU1Ka434fFUGGosCLJbGNySDhHGvxQWoRWK137FXYFkYNWw==", + "license": "Apache-2.0", + "dependencies": { + "@modelcontextprotocol/core": "2.3.1", + "zod": "^4.2.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/@napi-rs/wasm-runtime": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", @@ -3597,7 +3555,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "dev": true, "license": "MIT" }, "node_modules/@tybys/wasm-util": { @@ -6056,10 +6013,12 @@ } }, "node_modules/eventsource-parser": { - "version": "3.0.3", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.1.tgz", + "integrity": "sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ==", "license": "MIT", "engines": { - "node": ">=20.0.0" + "node": ">=18.0.0" } }, "node_modules/expand-tilde": { diff --git a/package.json b/package.json index 1c34c6d..3034649 100644 --- a/package.json +++ b/package.json @@ -51,8 +51,9 @@ "dependencies": { "@mapbox/mapbox-gl-style-spec": "^14.26.0", "@mcp-ui/server": "^6.1.0", - "@modelcontextprotocol/ext-apps": "^1.1.1", - "@modelcontextprotocol/sdk": "^1.30.0", + "@modelcontextprotocol/core": "^2.3.1", + "@modelcontextprotocol/ext-apps": "^2.0.3", + "@modelcontextprotocol/server": "^2.3.1", "@opentelemetry/api": "^1.9.1", "@opentelemetry/auto-instrumentations-node": "^0.74.0", "@opentelemetry/exporter-trace-otlp-http": "^0.216.0", @@ -65,6 +66,7 @@ }, "devDependencies": { "@eslint/js": "^9.27.0", + "@modelcontextprotocol/client": "^2.3.1", "@types/geojson": "^7946.0.16", "@types/node": "^22.0.0", "@typescript-eslint/eslint-plugin": "^8.58.2", diff --git a/patches/@modelcontextprotocol+sdk+1.30.0.patch b/patches/@modelcontextprotocol+sdk+1.30.0.patch deleted file mode 100644 index ca5949d..0000000 --- a/patches/@modelcontextprotocol+sdk+1.30.0.patch +++ /dev/null @@ -1,55 +0,0 @@ -diff --git a/node_modules/@modelcontextprotocol/sdk/dist/cjs/server/mcp.js b/node_modules/@modelcontextprotocol/sdk/dist/cjs/server/mcp.js -index 9e1c874..781ebad 100644 ---- a/node_modules/@modelcontextprotocol/sdk/dist/cjs/server/mcp.js -+++ b/node_modules/@modelcontextprotocol/sdk/dist/cjs/server/mcp.js -@@ -197,7 +197,7 @@ class McpServer { - return; - } - if (!result.structuredContent) { -- throw new types_js_1.McpError(types_js_1.ErrorCode.InvalidParams, `Output validation error: Tool ${toolName} has an output schema but no structured content was provided`); -+ console.warn(`[MCP SDK Patch] Output validation warning: Tool ${toolName} has an output schema but no structured content was provided`); - } - // if the tool has an output schema, validate structured content - const outputObj = (0, zod_compat_js_1.normalizeObjectSchema)(tool.outputSchema); -@@ -205,7 +205,7 @@ class McpServer { - if (!parseResult.success) { - const error = 'error' in parseResult ? parseResult.error : 'Unknown error'; - const errorMessage = (0, zod_compat_js_1.getParseErrorMessage)(error); -- throw new types_js_1.McpError(types_js_1.ErrorCode.InvalidParams, `Output validation error: Invalid structured content for tool ${toolName}: ${errorMessage}`); -+ console.warn(`[MCP SDK Patch] Output validation warning: Invalid structured content for tool ${toolName}: ${errorMessage}`); - } - } - /** -diff --git a/node_modules/@modelcontextprotocol/sdk/dist/esm/server/mcp.js b/node_modules/@modelcontextprotocol/sdk/dist/esm/server/mcp.js -index 2314d88..94aaef1 100644 ---- a/node_modules/@modelcontextprotocol/sdk/dist/esm/server/mcp.js -+++ b/node_modules/@modelcontextprotocol/sdk/dist/esm/server/mcp.js -@@ -194,15 +194,20 @@ export class McpServer { - return; - } - if (!result.structuredContent) { -- throw new McpError(ErrorCode.InvalidParams, `Output validation error: Tool ${toolName} has an output schema but no structured content was provided`); -+ // Log warning but don't throw - allow tools to omit structured content -+ console.warn(`[MCP SDK Patch] Output validation warning: Tool ${toolName} has an output schema but no structured content was provided`); - } -- // if the tool has an output schema, validate structured content -- const outputObj = normalizeObjectSchema(tool.outputSchema); -- const parseResult = await safeParseAsync(outputObj, result.structuredContent); -- if (!parseResult.success) { -- const error = 'error' in parseResult ? parseResult.error : 'Unknown error'; -- const errorMessage = getParseErrorMessage(error); -- throw new McpError(ErrorCode.InvalidParams, `Output validation error: Invalid structured content for tool ${toolName}: ${errorMessage}`); -+ else { -+ // if the tool has an output schema, validate structured content -+ const outputObj = normalizeObjectSchema(tool.outputSchema); -+ const parseResult = await safeParseAsync(outputObj, result.structuredContent); -+ if (!parseResult.success) { -+ const error = 'error' in parseResult ? parseResult.error : 'Unknown error'; -+ const errorMessage = getParseErrorMessage(error); -+ // Log warning but don't throw - allow schema mismatches -+ console.warn(`[MCP SDK Patch] Output validation warning: Invalid structured content for tool ${toolName}: ${errorMessage}`); -+ // Keep the structuredContent despite validation failure -+ } - } - } - /** diff --git a/patches/README.md b/patches/README.md index 1a6f86d..ce620a5 100644 --- a/patches/README.md +++ b/patches/README.md @@ -1,41 +1,17 @@ -# MCP SDK Patches +# Dependency Patches -This directory contains patches for npm dependencies that are automatically applied after `npm install`. +Patches in this directory are applied to npm dependencies after `npm install` by [patch-package](https://github.com/ds300/patch-package) (see the `postinstall` script). There are none at the moment. -## @modelcontextprotocol/sdk +## History -**File:** `@modelcontextprotocol+sdk+1.21.1.patch` +### @modelcontextprotocol/sdk (retired) -**Purpose:** Makes MCP output schema validation non-fatal to improve resilience and user experience. +Until the move to MCP TypeScript SDK v2, a patch to `@modelcontextprotocol/sdk` made output schema validation non-fatal: when a tool's `structuredContent` didn't match its output schema, or a tool with an output schema returned none, the SDK logged a warning instead of failing the tool call. -**Problem:** The MCP SDK enforces strict output schema validation, which causes the entire tool call to fail when there are minor schema mismatches. This creates unnecessary risk and poor user experience - most MCP clients can gracefully handle responses that don't perfectly match the declared schema. +SDK v2 accepts any [Standard Schema](https://standardschema.dev/) as a tool's `outputSchema`, so the same behavior now comes from `src/tools/lenientOutputSchema.ts` (the published JSON Schema is unchanged, validation only warns) and `BaseTool.installTo` (a missing `structuredContent` is sent as `{}`), with no patch. `test/tools/lenientOutputSchema.test.ts` covers both through a real `tools/call`. -**Solution:** This patch modifies the MCP SDK to log warnings instead of throwing errors when output validation fails. This provides a better balance between: +## Adding a patch -- **Schema documentation** - Output schemas still serve as documentation for expected response structure -- **Resilience** - Tools continue working even with minor variations in response format -- **Observability** - Validation issues are logged for monitoring and debugging -- **User experience** - Clients receive the actual data instead of an error - -**Benefits:** - -- Prevents tool failures due to minor schema variations -- Allows graceful degradation when APIs evolve or return edge cases -- Maintains backward compatibility while improving reliability -- Clients that need strict validation can implement it themselves - -**Changes:** - -- Converts validation errors to console warnings with `[MCP SDK Patch]` prefix -- Allows tools to return structured content even when it doesn't match the schema exactly -- Preserves all existing functionality while removing unnecessary strictness - -**Maintenance:** - -- This patch is automatically applied after `npm install` via the `postinstall` script -- If the MCP SDK is updated, you may need to recreate this patch: - 1. Remove the old patch file - 2. Make the same modifications to the new SDK version - 3. Run `npx patch-package @modelcontextprotocol/sdk` - -**Philosophy:** This patch follows the robustness principle: "Be conservative in what you send, be liberal in what you accept." Output schemas remain valuable for documentation and tooling, but shouldn't cause failures when the real-world data varies slightly from the ideal schema. +1. Edit the file in `node_modules//`. +2. Run `npx patch-package `. +3. Commit the generated file in this directory and describe it here. diff --git a/src/index.ts b/src/index.ts index b6a6ee0..51b85c2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,8 +9,8 @@ import { join } from 'node:path'; import { existsSync } from 'node:fs'; import { SpanStatusCode } from '@opentelemetry/api'; -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 { parseToolConfigFromArgs, filterTools } from './config/toolConfig.js'; import { getCoreTools, getElicitationTools } from './tools/toolRegistry.js'; @@ -104,7 +104,7 @@ prompts.forEach((prompt) => { prompt.name, { description: prompt.description, - argsSchema: argsSchema + argsSchema: z.object(argsSchema) }, async (args) => { // Filter out undefined values from optional arguments diff --git a/src/prompts/AnalyzeGeojsonPrompt.ts b/src/prompts/AnalyzeGeojsonPrompt.ts index 52a7f3b..bcd6e14 100644 --- a/src/prompts/AnalyzeGeojsonPrompt.ts +++ b/src/prompts/AnalyzeGeojsonPrompt.ts @@ -1,7 +1,7 @@ // Copyright (c) Mapbox, Inc. // Licensed under the MIT License. -import type { PromptMessage } from '@modelcontextprotocol/sdk/types.js'; +import type { PromptMessage } from '@modelcontextprotocol/server'; import { BasePrompt, type PromptArgument } from './BasePrompt.js'; /** diff --git a/src/prompts/BasePrompt.ts b/src/prompts/BasePrompt.ts index dbf287f..1a4630f 100644 --- a/src/prompts/BasePrompt.ts +++ b/src/prompts/BasePrompt.ts @@ -4,7 +4,7 @@ import type { PromptMessage, GetPromptResult -} from '@modelcontextprotocol/sdk/types.js'; +} from '@modelcontextprotocol/server'; /** * Argument definition for a prompt diff --git a/src/prompts/BuildCustomMapPrompt.ts b/src/prompts/BuildCustomMapPrompt.ts index b418d39..66ead76 100644 --- a/src/prompts/BuildCustomMapPrompt.ts +++ b/src/prompts/BuildCustomMapPrompt.ts @@ -1,7 +1,7 @@ // Copyright (c) Mapbox, Inc. // Licensed under the MIT License. -import type { PromptMessage } from '@modelcontextprotocol/sdk/types.js'; +import type { PromptMessage } from '@modelcontextprotocol/server'; import { BasePrompt, type PromptArgument } from './BasePrompt.js'; /** diff --git a/src/prompts/CreateAndPreviewStylePrompt.ts b/src/prompts/CreateAndPreviewStylePrompt.ts index 4a45eac..68f5b30 100644 --- a/src/prompts/CreateAndPreviewStylePrompt.ts +++ b/src/prompts/CreateAndPreviewStylePrompt.ts @@ -1,7 +1,7 @@ // Copyright (c) Mapbox, Inc. // Licensed under the MIT License. -import type { PromptMessage } from '@modelcontextprotocol/sdk/types.js'; +import type { PromptMessage } from '@modelcontextprotocol/server'; import { BasePrompt, type PromptArgument } from './BasePrompt.js'; /** diff --git a/src/prompts/DebugMapboxIntegrationPrompt.ts b/src/prompts/DebugMapboxIntegrationPrompt.ts index 00f1d89..15956f9 100644 --- a/src/prompts/DebugMapboxIntegrationPrompt.ts +++ b/src/prompts/DebugMapboxIntegrationPrompt.ts @@ -1,7 +1,7 @@ // Copyright (c) Mapbox, Inc. // Licensed under the MIT License. -import type { PromptMessage } from '@modelcontextprotocol/sdk/types.js'; +import type { PromptMessage } from '@modelcontextprotocol/server'; import { BasePrompt, type PromptArgument } from './BasePrompt.js'; /** diff --git a/src/prompts/DesignDataDrivenStylePrompt.ts b/src/prompts/DesignDataDrivenStylePrompt.ts index b09df1c..a4f4b6d 100644 --- a/src/prompts/DesignDataDrivenStylePrompt.ts +++ b/src/prompts/DesignDataDrivenStylePrompt.ts @@ -1,7 +1,7 @@ // Copyright (c) Mapbox, Inc. // Licensed under the MIT License. -import type { PromptMessage } from '@modelcontextprotocol/sdk/types.js'; +import type { PromptMessage } from '@modelcontextprotocol/server'; import { BasePrompt, type PromptArgument } from './BasePrompt.js'; /** diff --git a/src/prompts/PrepareStyleForProductionPrompt.ts b/src/prompts/PrepareStyleForProductionPrompt.ts index b2f26f5..1675c10 100644 --- a/src/prompts/PrepareStyleForProductionPrompt.ts +++ b/src/prompts/PrepareStyleForProductionPrompt.ts @@ -1,7 +1,7 @@ // Copyright (c) Mapbox, Inc. // Licensed under the MIT License. -import type { PromptMessage } from '@modelcontextprotocol/sdk/types.js'; +import type { PromptMessage } from '@modelcontextprotocol/server'; import { BasePrompt, type PromptArgument } from './BasePrompt.js'; /** diff --git a/src/prompts/SetupMapboxProjectPrompt.ts b/src/prompts/SetupMapboxProjectPrompt.ts index b1a9945..f02ebf6 100644 --- a/src/prompts/SetupMapboxProjectPrompt.ts +++ b/src/prompts/SetupMapboxProjectPrompt.ts @@ -1,7 +1,7 @@ // Copyright (c) Mapbox, Inc. // Licensed under the MIT License. -import type { PromptMessage } from '@modelcontextprotocol/sdk/types.js'; +import type { PromptMessage } from '@modelcontextprotocol/server'; import { BasePrompt, type PromptArgument } from './BasePrompt.js'; /** diff --git a/src/resources/BaseResource.ts b/src/resources/BaseResource.ts index a89e634..13e71e3 100644 --- a/src/resources/BaseResource.ts +++ b/src/resources/BaseResource.ts @@ -1,13 +1,11 @@ // Copyright (c) Mapbox, Inc. // Licensed under the MIT License. -import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import type { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol.js'; import type { + McpServer, ReadResourceResult, - ServerNotification, - ServerRequest -} from '@modelcontextprotocol/sdk/types.js'; + ServerContext +} from '@modelcontextprotocol/server'; /** * Base class for MCP resources @@ -38,6 +36,6 @@ export abstract class BaseResource { */ public abstract readCallback( uri: URL, - extra: RequestHandlerExtra + extra: ServerContext ): Promise; } diff --git a/src/resources/resourceRegistry.ts b/src/resources/resourceRegistry.ts index 637a2bd..3e6ca9c 100644 --- a/src/resources/resourceRegistry.ts +++ b/src/resources/resourceRegistry.ts @@ -1,7 +1,7 @@ // Copyright (c) Mapbox, Inc. // Licensed under the MIT License. -import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import type { McpServer } from '@modelcontextprotocol/server'; import type { BaseResource } from './BaseResource.js'; import { MapboxStyleLayersResource } from './mapbox-style-layers-resource/MapboxStyleLayersResource.js'; import { MapboxStreetsV8FieldsResource } from './mapbox-streets-v8-fields-resource/MapboxStreetsV8FieldsResource.js'; @@ -30,7 +30,7 @@ export function getAllResources(): readonly ResourceInstance[] { /** * Registers every resource on `server` via `BaseResource.installTo`, so each * read callback receives the request's `extra` (including the caller's - * `authInfo`). UI resources declare `RESOURCE_MIME_TYPE` themselves, which is + * `http.authInfo`). UI resources declare `RESOURCE_MIME_TYPE` themselves, which is * all `registerAppResource` would add. */ export function installResources( diff --git a/src/resources/ui-apps/MapPreviewUIResource.ts b/src/resources/ui-apps/MapPreviewUIResource.ts index 220ce81..09b611f 100644 --- a/src/resources/ui-apps/MapPreviewUIResource.ts +++ b/src/resources/ui-apps/MapPreviewUIResource.ts @@ -1,12 +1,10 @@ // Copyright (c) Mapbox, Inc. // Licensed under the MIT License. -import type { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol.js'; import type { ReadResourceResult, - ServerNotification, - ServerRequest -} from '@modelcontextprotocol/sdk/types.js'; + ServerContext +} from '@modelcontextprotocol/server'; import { RESOURCE_MIME_TYPE } from '@modelcontextprotocol/ext-apps/server'; import { BaseResource } from '../BaseResource.js'; import { mintScopedPreviewToken } from '../../utils/mintScopedPreviewToken.js'; @@ -39,13 +37,13 @@ export class MapPreviewUIResource extends BaseResource { public async readCallback( _uri: URL, - _extra: RequestHandlerExtra + _extra: ServerContext ): Promise { // GL JS requires a public (pk.*) token. Create a short-lived one on the // customer's account using their sk.* token so we're not exposing any // Mapbox-owned credentials. Falls back gracefully if no sk.* is configured. const skToken = - (_extra.authInfo?.token as string | undefined) || + (_extra.http?.authInfo?.token as string | undefined) || process.env.MAPBOX_ACCESS_TOKEN || ''; let accessToken = ''; diff --git a/src/resources/ui-apps/StyleComparisonUIResource.ts b/src/resources/ui-apps/StyleComparisonUIResource.ts index af9de78..ac01cdf 100644 --- a/src/resources/ui-apps/StyleComparisonUIResource.ts +++ b/src/resources/ui-apps/StyleComparisonUIResource.ts @@ -1,12 +1,10 @@ // Copyright (c) Mapbox, Inc. // Licensed under the MIT License. -import type { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol.js'; import type { ReadResourceResult, - ServerNotification, - ServerRequest -} from '@modelcontextprotocol/sdk/types.js'; + ServerContext +} from '@modelcontextprotocol/server'; import { RESOURCE_MIME_TYPE } from '@modelcontextprotocol/ext-apps/server'; import { BaseResource } from '../BaseResource.js'; @@ -28,7 +26,7 @@ export class StyleComparisonUIResource extends BaseResource { public async readCallback( _uri: URL, - _extra: RequestHandlerExtra + _extra: ServerContext ): Promise { const html = ` diff --git a/src/tools/BaseTool.ts b/src/tools/BaseTool.ts index 5970aea..b71b760 100644 --- a/src/tools/BaseTool.ts +++ b/src/tools/BaseTool.ts @@ -3,15 +3,14 @@ import { McpServer, - RegisteredTool -} from '@modelcontextprotocol/sdk/server/mcp.js'; -import { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol.js'; -import { + RegisteredTool, CallToolResult, - ToolAnnotations -} from '@modelcontextprotocol/sdk/types.js'; + ToolAnnotations, + ServerContext +} from '@modelcontextprotocol/server'; import { z, ZodTypeAny } from 'zod'; import type { ToolExecutionContext } from '../utils/tracing.js'; +import { lenientOutputSchema } from './lenientOutputSchema.js'; export abstract class BaseTool< InputSchema extends ZodTypeAny, @@ -46,15 +45,11 @@ export abstract class BaseTool< /** * Tool logic to be implemented by subclasses. */ - async run( - rawInput: unknown, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - extra?: RequestHandlerExtra - ): Promise { + async run(rawInput: unknown, extra?: ServerContext): Promise { try { const input = this.inputSchema.parse(rawInput); const accessToken = - extra?.authInfo?.token || process.env.MAPBOX_ACCESS_TOKEN; + extra?.http?.authInfo?.token || process.env.MAPBOX_ACCESS_TOKEN; return this.execute(input, accessToken); } catch (error) { return { @@ -104,11 +99,9 @@ export abstract class BaseTool< // Add outputSchema if provided if (this.outputSchema) { - // Pass the schema shape directly - don't wrap - // The MCP SDK will validate structuredContent against this schema - config.outputSchema = - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (this.outputSchema as unknown as z.ZodObject).shape; + // tools/list publishes the real schema; validation only warns. See + // lenientOutputSchema.ts. + config.outputSchema = lenientOutputSchema(this.outputSchema, this.name); } // Add _meta for MCP Apps support if provided (includes CSP configuration) @@ -120,7 +113,23 @@ export abstract class BaseTool< this.name, config, // eslint-disable-next-line @typescript-eslint/no-explicit-any - (args: any, extra: any) => this.run(args, extra) + async (args: any, ctx: ServerContext) => { + const result = await this.run(args, ctx); + // McpServer fails a call to a tool with an output schema that returns + // no structuredContent, before validation can let it through. Send an + // empty object rather than turn a usable text answer into an error. + if ( + this.outputSchema && + !result.isError && + result.structuredContent === undefined + ) { + console.warn( + `Output validation warning: tool ${this.name} has an output schema but returned no structured content` + ); + return { ...result, structuredContent: {} }; + } + return result; + } ); } diff --git a/src/tools/MapboxApiBasedTool.ts b/src/tools/MapboxApiBasedTool.ts index 2e8bde3..d40cee4 100644 --- a/src/tools/MapboxApiBasedTool.ts +++ b/src/tools/MapboxApiBasedTool.ts @@ -1,13 +1,13 @@ // Copyright (c) Mapbox, Inc. // Licensed under the MIT License. -import type { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol.js'; -import type { ZodTypeAny, z } from 'zod'; -import { BaseTool } from './BaseTool.js'; import type { CallToolResult, - ToolAnnotations -} from '@modelcontextprotocol/sdk/types.js'; + ToolAnnotations, + ServerContext +} from '@modelcontextprotocol/server'; +import type { ZodTypeAny, z } from 'zod'; +import { BaseTool } from './BaseTool.js'; import type { HttpRequest } from '../utils/types.js'; import { context, trace, SpanStatusCode } from '@opentelemetry/api'; import type { ToolExecutionContext } from '../utils/tracing.js'; @@ -127,16 +127,12 @@ export abstract class MapboxApiBasedTool< /** * Validates and runs the tool logic. */ - async run( - rawInput: unknown, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - extra?: RequestHandlerExtra - ): Promise { + async run(rawInput: unknown, extra?: ServerContext): Promise { // First check if token is provided via authentication context // Check both standard token field and accessToken in extra for compatibility // In the streamableHttp, the authInfo is injected into extra from `req.auth` // https://github.com/modelcontextprotocol/typescript-sdk/blob/main/src/server/streamableHttp.ts#L405 - const authToken = extra?.authInfo?.token; + const authToken = extra?.http?.authInfo?.token; const accessToken = authToken || MapboxApiBasedTool.mapboxAccessToken; if (!accessToken) { const errorMessage = diff --git a/src/tools/bounding-box-tool/BoundingBoxTool.ts b/src/tools/bounding-box-tool/BoundingBoxTool.ts index b5d9900..80fcc7b 100644 --- a/src/tools/bounding-box-tool/BoundingBoxTool.ts +++ b/src/tools/bounding-box-tool/BoundingBoxTool.ts @@ -1,7 +1,7 @@ // Copyright (c) Mapbox, Inc. // Licensed under the MIT License. -import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; +import type { CallToolResult } from '@modelcontextprotocol/server'; import type { GeoJSON, Feature, diff --git a/src/tools/bounding-box-tool/CountryBoundingBoxTool.ts b/src/tools/bounding-box-tool/CountryBoundingBoxTool.ts index 57c2dc6..1ef1dc3 100644 --- a/src/tools/bounding-box-tool/CountryBoundingBoxTool.ts +++ b/src/tools/bounding-box-tool/CountryBoundingBoxTool.ts @@ -1,7 +1,7 @@ // Copyright (c) Mapbox, Inc. // Licensed under the MIT License. -import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; +import type { CallToolResult } from '@modelcontextprotocol/server'; import { BaseTool } from '../BaseTool.js'; import { CountryBoundingBoxSchema, diff --git a/src/tools/check-color-contrast-tool/CheckColorContrastTool.ts b/src/tools/check-color-contrast-tool/CheckColorContrastTool.ts index 3330761..38de701 100644 --- a/src/tools/check-color-contrast-tool/CheckColorContrastTool.ts +++ b/src/tools/check-color-contrast-tool/CheckColorContrastTool.ts @@ -3,7 +3,7 @@ import { z } from 'zod'; import { BaseTool } from '../BaseTool.js'; -import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; +import type { CallToolResult } from '@modelcontextprotocol/server'; import { CheckColorContrastInputSchema } from './CheckColorContrastTool.input.schema.js'; import { CheckColorContrastOutputSchema, diff --git a/src/tools/compare-styles-tool/CompareStylesTool.ts b/src/tools/compare-styles-tool/CompareStylesTool.ts index 96a2655..dd9de80 100644 --- a/src/tools/compare-styles-tool/CompareStylesTool.ts +++ b/src/tools/compare-styles-tool/CompareStylesTool.ts @@ -3,7 +3,7 @@ import { z } from 'zod'; import { BaseTool } from '../BaseTool.js'; -import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; +import type { CallToolResult } from '@modelcontextprotocol/server'; import { CompareStylesInputSchema } from './CompareStylesTool.input.schema.js'; import { CompareStylesOutputSchema, diff --git a/src/tools/coordinate-conversion-tool/CoordinateConversionTool.ts b/src/tools/coordinate-conversion-tool/CoordinateConversionTool.ts index c14f5a0..a32ca97 100644 --- a/src/tools/coordinate-conversion-tool/CoordinateConversionTool.ts +++ b/src/tools/coordinate-conversion-tool/CoordinateConversionTool.ts @@ -1,7 +1,7 @@ // Copyright (c) Mapbox, Inc. // Licensed under the MIT License. -import { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; +import { CallToolResult } from '@modelcontextprotocol/server'; import { BaseTool } from '../BaseTool.js'; import { CoordinateConversionOutput, diff --git a/src/tools/create-style-tool/CreateStyleTool.ts b/src/tools/create-style-tool/CreateStyleTool.ts index f300b63..6cb7ddf 100644 --- a/src/tools/create-style-tool/CreateStyleTool.ts +++ b/src/tools/create-style-tool/CreateStyleTool.ts @@ -1,7 +1,7 @@ // Copyright (c) Mapbox, Inc. // Licensed under the MIT License. -import { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; +import { CallToolResult } from '@modelcontextprotocol/server'; import type { HttpRequest } from '../../utils/types.js'; import type { ToolExecutionContext } from '../../utils/tracing.js'; import { getUserNameFromToken } from '../../utils/jwtUtils.js'; diff --git a/src/tools/create-token-tool/CreateTokenTool.ts b/src/tools/create-token-tool/CreateTokenTool.ts index ed374ad..265b0b8 100644 --- a/src/tools/create-token-tool/CreateTokenTool.ts +++ b/src/tools/create-token-tool/CreateTokenTool.ts @@ -1,7 +1,7 @@ // Copyright (c) Mapbox, Inc. // Licensed under the MIT License. -import { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; +import { CallToolResult } from '@modelcontextprotocol/server'; import type { HttpRequest } from '../../utils/types.js'; import type { ToolExecutionContext } from '../../utils/tracing.js'; import { MapboxApiBasedTool } from '../MapboxApiBasedTool.js'; diff --git a/src/tools/delete-style-tool/DeleteStyleTool.ts b/src/tools/delete-style-tool/DeleteStyleTool.ts index abbd6f9..4d2e5fa 100644 --- a/src/tools/delete-style-tool/DeleteStyleTool.ts +++ b/src/tools/delete-style-tool/DeleteStyleTool.ts @@ -1,7 +1,7 @@ // Copyright (c) Mapbox, Inc. // Licensed under the MIT License. -import { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; +import { CallToolResult } from '@modelcontextprotocol/server'; import type { HttpRequest } from '../../utils/types.js'; import type { ToolExecutionContext } from '../../utils/tracing.js'; import { getUserNameFromToken } from '../../utils/jwtUtils.js'; diff --git a/src/tools/geojson-preview-tool/GeojsonPreviewTool.ts b/src/tools/geojson-preview-tool/GeojsonPreviewTool.ts index 4a76641..0058d49 100644 --- a/src/tools/geojson-preview-tool/GeojsonPreviewTool.ts +++ b/src/tools/geojson-preview-tool/GeojsonPreviewTool.ts @@ -1,7 +1,7 @@ // Copyright (c) Mapbox, Inc. // Licensed under the MIT License. -import { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; +import { CallToolResult } from '@modelcontextprotocol/server'; import { createUIResource } from '@mcp-ui/server'; import { createHash, randomUUID } from 'node:crypto'; import { GeoJSON } from 'geojson'; diff --git a/src/tools/get-feedback-tool/GetFeedbackTool.ts b/src/tools/get-feedback-tool/GetFeedbackTool.ts index 98d2a94..f834824 100644 --- a/src/tools/get-feedback-tool/GetFeedbackTool.ts +++ b/src/tools/get-feedback-tool/GetFeedbackTool.ts @@ -3,7 +3,7 @@ import type { z } from 'zod'; import { MapboxApiBasedTool } from '../MapboxApiBasedTool.js'; -import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; +import type { CallToolResult } from '@modelcontextprotocol/server'; import type { HttpRequest } from '../../utils/types.js'; import { GetFeedbackInputSchema } from './GetFeedbackTool.input.schema.js'; import { FeedbackGetResponseSchema } from '../feedback.schema.js'; diff --git a/src/tools/lenientOutputSchema.ts b/src/tools/lenientOutputSchema.ts new file mode 100644 index 0000000..c81b8bb --- /dev/null +++ b/src/tools/lenientOutputSchema.ts @@ -0,0 +1,41 @@ +// Copyright (c) Mapbox, Inc. +// Licensed under the MIT License. + +import type { StandardSchemaWithJSON } from '@modelcontextprotocol/server'; +import type { ZodTypeAny } from 'zod'; + +/** + * Wraps a tool's output schema so McpServer still publishes it in + * `tools/list` but doesn't fail the call when `structuredContent` doesn't + * match it. + * + * McpServer turns an output-schema mismatch into an error result, which + * throws away a response the model could have used because an upstream API + * added or changed a field. SDK v1 needed a patch to the SDK for this (see + * patches/README.md); v2 takes any Standard Schema, so this keeps the + * schema's JSON Schema and swaps in a validator that logs the mismatch to + * stderr and passes the data through unchanged. + */ +export function lenientOutputSchema( + schema: ZodTypeAny, + toolName: string +): StandardSchemaWithJSON { + const standard = (schema as unknown as StandardSchemaWithJSON)['~standard']; + return { + '~standard': { + version: 1, + vendor: standard.vendor, + jsonSchema: standard.jsonSchema, + validate: async (value: unknown) => { + const result = await standard.validate(value); + if (result.issues?.length) { + console.warn( + `Output validation warning: structured content for tool ${toolName} doesn't match its output schema: ` + + result.issues.map((issue) => issue.message).join(', ') + ); + } + return { value }; + } + } + } as StandardSchemaWithJSON; +} diff --git a/src/tools/list-feedback-tool/ListFeedbackTool.ts b/src/tools/list-feedback-tool/ListFeedbackTool.ts index ebf4725..c9b0e74 100644 --- a/src/tools/list-feedback-tool/ListFeedbackTool.ts +++ b/src/tools/list-feedback-tool/ListFeedbackTool.ts @@ -3,7 +3,7 @@ import type { z } from 'zod'; import { MapboxApiBasedTool } from '../MapboxApiBasedTool.js'; -import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; +import type { CallToolResult } from '@modelcontextprotocol/server'; import type { HttpRequest } from '../../utils/types.js'; import { ListFeedbackInputSchema } from './ListFeedbackTool.input.schema.js'; import { FeedbackListResponseSchema } from '../feedback.schema.js'; diff --git a/src/tools/list-styles-tool/ListStylesTool.ts b/src/tools/list-styles-tool/ListStylesTool.ts index fa924f7..5a2d529 100644 --- a/src/tools/list-styles-tool/ListStylesTool.ts +++ b/src/tools/list-styles-tool/ListStylesTool.ts @@ -1,7 +1,7 @@ // Copyright (c) Mapbox, Inc. // Licensed under the MIT License. -import { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; +import { CallToolResult } from '@modelcontextprotocol/server'; import type { HttpRequest } from '../../utils/types.js'; import type { ToolExecutionContext } from '../../utils/tracing.js'; import { MapboxApiBasedTool } from '../MapboxApiBasedTool.js'; diff --git a/src/tools/list-tokens-tool/ListTokensTool.ts b/src/tools/list-tokens-tool/ListTokensTool.ts index 44345fd..59d6c23 100644 --- a/src/tools/list-tokens-tool/ListTokensTool.ts +++ b/src/tools/list-tokens-tool/ListTokensTool.ts @@ -1,7 +1,7 @@ // Copyright (c) Mapbox, Inc. // Licensed under the MIT License. -import { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; +import { CallToolResult } from '@modelcontextprotocol/server'; import type { HttpRequest } from '../../utils/types.js'; import type { ToolExecutionContext } from '../../utils/tracing.js'; import { MapboxApiBasedTool, redactToken } from '../MapboxApiBasedTool.js'; diff --git a/src/tools/optimize-style-tool/OptimizeStyleTool.ts b/src/tools/optimize-style-tool/OptimizeStyleTool.ts index 2608074..8833da9 100644 --- a/src/tools/optimize-style-tool/OptimizeStyleTool.ts +++ b/src/tools/optimize-style-tool/OptimizeStyleTool.ts @@ -3,7 +3,7 @@ import { z } from 'zod'; import { BaseTool } from '../BaseTool.js'; -import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; +import type { CallToolResult } from '@modelcontextprotocol/server'; import { OptimizeStyleInputSchema } from './OptimizeStyleTool.input.schema.js'; import { OptimizeStyleOutputSchema, diff --git a/src/tools/preview-style-tool/PreviewStyleTool.ts b/src/tools/preview-style-tool/PreviewStyleTool.ts index de7544c..c69a923 100644 --- a/src/tools/preview-style-tool/PreviewStyleTool.ts +++ b/src/tools/preview-style-tool/PreviewStyleTool.ts @@ -1,5 +1,5 @@ import { randomUUID } from 'node:crypto'; -import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; +import type { CallToolResult } from '@modelcontextprotocol/server'; import { createUIResource } from '@mcp-ui/server'; import { BaseTool } from '../BaseTool.js'; import { MapboxApiBasedTool } from '../MapboxApiBasedTool.js'; diff --git a/src/tools/retrieve-style-tool/RetrieveStyleTool.ts b/src/tools/retrieve-style-tool/RetrieveStyleTool.ts index 6e5091f..d00a980 100644 --- a/src/tools/retrieve-style-tool/RetrieveStyleTool.ts +++ b/src/tools/retrieve-style-tool/RetrieveStyleTool.ts @@ -10,7 +10,7 @@ import { RetrieveStyleInput } from './RetrieveStyleTool.input.schema.js'; import { HttpRequest } from '../../utils/types.js'; -import { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; +import { CallToolResult } from '@modelcontextprotocol/server'; import { MapboxStyleOutput, MapboxStyleOutputSchema diff --git a/src/tools/style-builder-tool/StyleBuilderTool.ts b/src/tools/style-builder-tool/StyleBuilderTool.ts index 0f093f5..30de165 100644 --- a/src/tools/style-builder-tool/StyleBuilderTool.ts +++ b/src/tools/style-builder-tool/StyleBuilderTool.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. import { BaseTool } from '../BaseTool.js'; -import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; +import type { CallToolResult } from '@modelcontextprotocol/server'; import { StyleBuilderToolSchema, type StyleBuilderToolInput diff --git a/src/tools/style-comparison-tool/StyleComparisonTool.ts b/src/tools/style-comparison-tool/StyleComparisonTool.ts index f925957..5fc064d 100644 --- a/src/tools/style-comparison-tool/StyleComparisonTool.ts +++ b/src/tools/style-comparison-tool/StyleComparisonTool.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. import { randomUUID } from 'node:crypto'; -import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; +import type { CallToolResult } from '@modelcontextprotocol/server'; import { createUIResource } from '@mcp-ui/server'; import { BaseTool } from '../BaseTool.js'; import { diff --git a/src/tools/tilequery-tool/TilequeryTool.ts b/src/tools/tilequery-tool/TilequeryTool.ts index 9800e9c..ba2e2d7 100644 --- a/src/tools/tilequery-tool/TilequeryTool.ts +++ b/src/tools/tilequery-tool/TilequeryTool.ts @@ -1,7 +1,7 @@ // Copyright (c) Mapbox, Inc. // Licensed under the MIT License. -import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; +import type { CallToolResult } from '@modelcontextprotocol/server'; import type { HttpRequest } from '../../utils/types.js'; import type { ToolExecutionContext } from '../../utils/tracing.js'; import { MapboxApiBasedTool } from '../MapboxApiBasedTool.js'; diff --git a/src/tools/update-style-tool/UpdateStyleTool.ts b/src/tools/update-style-tool/UpdateStyleTool.ts index 0b613fe..5b09684 100644 --- a/src/tools/update-style-tool/UpdateStyleTool.ts +++ b/src/tools/update-style-tool/UpdateStyleTool.ts @@ -10,7 +10,7 @@ import { UpdateStyleInputSchema } from './UpdateStyleTool.input.schema.js'; import { getUserNameFromToken } from '../../utils/jwtUtils.js'; -import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; +import type { CallToolResult } from '@modelcontextprotocol/server'; import { MapboxStyleOutputSchema, MapboxStyleOutput diff --git a/src/tools/validate-expression-tool/ValidateExpressionTool.ts b/src/tools/validate-expression-tool/ValidateExpressionTool.ts index 82901ca..45ed3a4 100644 --- a/src/tools/validate-expression-tool/ValidateExpressionTool.ts +++ b/src/tools/validate-expression-tool/ValidateExpressionTool.ts @@ -5,7 +5,7 @@ import { z } from 'zod'; import { expression as expressionSpec } from '@mapbox/mapbox-gl-style-spec'; import type { StylePropertySpecification } from '@mapbox/mapbox-gl-style-spec'; import { BaseTool } from '../BaseTool.js'; -import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; +import type { CallToolResult } from '@modelcontextprotocol/server'; import { ValidateExpressionInputSchema } from './ValidateExpressionTool.input.schema.js'; import { ValidateExpressionOutputSchema, diff --git a/src/tools/validate-geojson-tool/ValidateGeojsonTool.ts b/src/tools/validate-geojson-tool/ValidateGeojsonTool.ts index 229b334..e133cae 100644 --- a/src/tools/validate-geojson-tool/ValidateGeojsonTool.ts +++ b/src/tools/validate-geojson-tool/ValidateGeojsonTool.ts @@ -3,7 +3,7 @@ import { z } from 'zod'; import { BaseTool } from '../BaseTool.js'; -import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; +import type { CallToolResult } from '@modelcontextprotocol/server'; import { ValidateGeojsonInputSchema } from './ValidateGeojsonTool.input.schema.js'; import { ValidateGeojsonOutputSchema, diff --git a/src/tools/validate-style-tool/ValidateStyleTool.ts b/src/tools/validate-style-tool/ValidateStyleTool.ts index ea32929..62c4455 100644 --- a/src/tools/validate-style-tool/ValidateStyleTool.ts +++ b/src/tools/validate-style-tool/ValidateStyleTool.ts @@ -8,7 +8,7 @@ import type { ValidationError as MapboxValidationError } from '@mapbox/mapbox-gl-style-spec'; import { BaseTool } from '../BaseTool.js'; -import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; +import type { CallToolResult } from '@modelcontextprotocol/server'; import { ValidateStyleInputSchema } from './ValidateStyleTool.input.schema.js'; import { ValidateStyleOutputSchema, diff --git a/test/integration/loggingOrder.test.ts b/test/integration/loggingOrder.test.ts index cc058c7..448d589 100644 --- a/test/integration/loggingOrder.test.ts +++ b/test/integration/loggingOrder.test.ts @@ -5,9 +5,8 @@ import { describe, it, expect } from 'vitest'; import { existsSync } from 'node:fs'; import { join, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; -import { Client } from '@modelcontextprotocol/sdk/client/index.js'; -import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; -import { LoggingMessageNotificationSchema } from '@modelcontextprotocol/sdk/types.js'; +import { StdioClientTransport } from '@modelcontextprotocol/client/stdio'; +import { Client } from '@modelcontextprotocol/client'; /** * Spawns the *actual built server* (dist/esm/index.js) as a real child @@ -46,7 +45,7 @@ describe.skipIf(!existsSync(SERVER_ENTRY))( const received: unknown[] = []; client.setNotificationHandler( - LoggingMessageNotificationSchema, + 'notifications/message', async (notification) => { received.push(notification.params); } diff --git a/test/resources/resourceRegistry.test.ts b/test/resources/resourceRegistry.test.ts index 067836d..2867d1a 100644 --- a/test/resources/resourceRegistry.test.ts +++ b/test/resources/resourceRegistry.test.ts @@ -2,10 +2,9 @@ // Licensed under the MIT License. import { describe, it, expect, vi, afterEach } from 'vitest'; -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import { Client } from '@modelcontextprotocol/sdk/client/index.js'; -import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js'; -import type { JSONRPCMessage } from '@modelcontextprotocol/sdk/types.js'; +import { Client } from '@modelcontextprotocol/client'; +import { McpServer, InMemoryTransport } from '@modelcontextprotocol/server'; +import type { JSONRPCMessage } from '@modelcontextprotocol/server'; import { RESOURCE_MIME_TYPE } from '@modelcontextprotocol/ext-apps/server'; import { getAllResources, @@ -67,7 +66,7 @@ describe('installResources', () => { spies.forEach((spy) => { expect(spy).toHaveBeenCalledTimes(1); - expect(spy.mock.calls[0][1].authInfo?.token).toBe('caller-token'); + expect(spy.mock.calls[0][1].http?.authInfo?.token).toBe('caller-token'); }); await client.close(); }); diff --git a/test/resources/ui-apps/MapPreviewUIResource.test.ts b/test/resources/ui-apps/MapPreviewUIResource.test.ts index 7322f66..4197773 100644 --- a/test/resources/ui-apps/MapPreviewUIResource.test.ts +++ b/test/resources/ui-apps/MapPreviewUIResource.test.ts @@ -22,7 +22,7 @@ function embeddedToken(html: string): string | null { // eslint-disable-next-line @typescript-eslint/no-explicit-any function extra(token?: string): any { - return token ? { authInfo: { token } } : {}; + return token ? { http: { authInfo: { token } } } : {}; } async function readHtml( diff --git a/test/security/path-traversal.test.ts b/test/security/path-traversal.test.ts index e26bca8..75e0366 100644 --- a/test/security/path-traversal.test.ts +++ b/test/security/path-traversal.test.ts @@ -189,7 +189,7 @@ describe('path traversal security', () => { await new DeleteStyleTool({ httpRequest }).run( { styleId: VALID_STYLE_ID }, // eslint-disable-next-line @typescript-eslint/no-explicit-any - { authInfo: { token: maliciousToken } } as any + { http: { authInfo: { token: maliciousToken } } } as any ); const calledUrl: string = mockHttpRequest.mock.calls[0][0]; @@ -221,7 +221,7 @@ describe('path traversal security', () => { await new RetrieveStyleTool({ httpRequest }).run( { styleId: VALID_STYLE_ID }, // eslint-disable-next-line @typescript-eslint/no-explicit-any - { authInfo: { token: maliciousToken } } as any + { http: { authInfo: { token: maliciousToken } } } as any ); const calledUrl: string = mockHttpRequest.mock.calls[0][0]; @@ -239,7 +239,7 @@ describe('path traversal security', () => { await new UpdateStyleTool({ httpRequest }).run( { styleId: VALID_STYLE_ID }, // eslint-disable-next-line @typescript-eslint/no-explicit-any - { authInfo: { token: maliciousToken } } as any + { http: { authInfo: { token: maliciousToken } } } as any ); const calledUrl: string = mockHttpRequest.mock.calls[0][0]; diff --git a/test/tools/MapboxApiBasedTool.test.ts b/test/tools/MapboxApiBasedTool.test.ts index 606cb33..6949bd3 100644 --- a/test/tools/MapboxApiBasedTool.test.ts +++ b/test/tools/MapboxApiBasedTool.test.ts @@ -10,7 +10,7 @@ import { MapboxApiBasedTool, redactToken } from '../../src/tools/MapboxApiBasedTool.js'; -import { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; +import { CallToolResult } from '@modelcontextprotocol/server'; import type { HttpRequest } from '../../src/utils/types.js'; import { setupHttpRequest } from '../utils/httpPipelineUtils.js'; diff --git a/test/tools/lenientOutputSchema.test.ts b/test/tools/lenientOutputSchema.test.ts new file mode 100644 index 0000000..3795c1d --- /dev/null +++ b/test/tools/lenientOutputSchema.test.ts @@ -0,0 +1,130 @@ +// Copyright (c) Mapbox, Inc. +// Licensed under the MIT License. + +import { describe, it, expect, vi, afterEach } from 'vitest'; +import { z } from 'zod'; +import { McpServer, InMemoryTransport } from '@modelcontextprotocol/server'; +import type { + CallToolResult, + ToolAnnotations +} from '@modelcontextprotocol/server'; +import { Client } from '@modelcontextprotocol/client'; +import { BaseTool } from '../../src/tools/BaseTool.js'; + +const InputSchema = z.object({ input: z.string() }); +const OutputSchema = z.object({ + name: z.string(), + items: z.array(z.string()) +}); + +/** A tool whose execute() returns whatever result the test gives it. */ +class FixedResultTool extends BaseTool< + typeof InputSchema, + typeof OutputSchema +> { + readonly name = 'fixed_result_tool'; + readonly description = 'Returns a fixed result'; + readonly annotations: ToolAnnotations = { title: 'Fixed result' }; + + constructor(private readonly result: CallToolResult) { + super({ inputSchema: InputSchema, outputSchema: OutputSchema }); + } + + protected async execute(): Promise { + return this.result; + } +} + +/** + * Calls the tool through a real McpServer. Uses a raw tools/call request, + * since the SDK client's callTool() runs its own output-schema check and + * this is about what the server sends. + */ +async function callThroughServer(result: CallToolResult) { + const server = new McpServer({ name: 'test', version: '0.0.0' }); + new FixedResultTool(result).installTo(server); + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + const client = new Client({ name: 'test-client', version: '0.0.0' }); + await Promise.all([ + server.connect(serverTransport), + client.connect(clientTransport) + ]); + const { tools } = await client.listTools(); + const response = (await client.request({ + method: 'tools/call', + params: { name: 'fixed_result_tool', arguments: { input: 'x' } } + })) as CallToolResult; + await client.close(); + return { tools, response }; +} + +describe('lenient output schema', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('publishes the real output schema in tools/list', async () => { + const { tools } = await callThroughServer({ + content: [{ type: 'text', text: 'ok' }], + structuredContent: { name: 'a', items: [] } + }); + expect(tools[0].outputSchema).toMatchObject({ + type: 'object', + properties: { + name: { type: 'string' }, + items: { type: 'array', items: { type: 'string' } } + }, + required: ['name', 'items'] + }); + }); + + it('passes mismatched structured content through with a warning, not an error', async () => { + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); + const { response } = await callThroughServer({ + content: [{ type: 'text', text: 'Processed' }], + structuredContent: { name: 'a', items: 'not-an-array' } + }); + + expect(response.isError).toBeFalsy(); + expect(response.structuredContent).toEqual({ + name: 'a', + items: 'not-an-array' + }); + expect(response.content).toEqual([{ type: 'text', text: 'Processed' }]); + expect(warn).toHaveBeenCalledWith( + expect.stringContaining('fixed_result_tool') + ); + }); + + it('keeps a text-only answer instead of failing for missing structured content', async () => { + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); + const { response } = await callThroughServer({ + content: [{ type: 'text', text: 'Text only' }] + }); + + expect(response.isError).toBeFalsy(); + expect(response.content).toEqual([{ type: 'text', text: 'Text only' }]); + expect(response.structuredContent).toEqual({}); + expect(warn).toHaveBeenCalledWith( + expect.stringContaining('no structured content') + ); + }); + + it('leaves matching structured content and error results alone', async () => { + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); + const ok = await callThroughServer({ + content: [{ type: 'text', text: 'ok' }], + structuredContent: { name: 'a', items: ['b'] } + }); + expect(ok.response.structuredContent).toEqual({ name: 'a', items: ['b'] }); + + const failed = await callThroughServer({ + content: [{ type: 'text', text: 'Upstream failed' }], + isError: true + }); + expect(failed.response.isError).toBe(true); + expect(failed.response.structuredContent).toBeUndefined(); + expect(warn).not.toHaveBeenCalled(); + }); +}); From cad0f4051bbcc8cb7c64a344445843ea66f62cce Mon Sep 17 00:00:00 2001 From: Matthew Podwysocki Date: Wed, 21 Oct 2026 15:33:44 -0400 Subject: [PATCH 2/4] docs: add PR number to CHANGELOG Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e7cd591..abe153a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,7 +18,7 @@ ### Breaking Changes -- **MCP TypeScript SDK v2 (PR number TBD; same move as mapbox/mcp-server#296).** The server and the `@mapbox/mcp-devkit-server/tools`, `/resources` and `/prompts` exports now use the v2 SDK packages (`@modelcontextprotocol/server` and `@modelcontextprotocol/core`, 2.3.1) instead of `@modelcontextprotocol/sdk` 1.x, and `@modelcontextprotocol/ext-apps` moves from 1.x to 2.0.3. v2 is where new MCP features land. +- **MCP TypeScript SDK v2 (#142, same move as mapbox/mcp-server#296).** The server and the `@mapbox/mcp-devkit-server/tools`, `/resources` and `/prompts` exports now use the v2 SDK packages (`@modelcontextprotocol/server` and `@modelcontextprotocol/core`, 2.3.1) instead of `@modelcontextprotocol/sdk` 1.x, and `@modelcontextprotocol/ext-apps` moves from 1.x to 2.0.3. v2 is where new MCP features land. - **For code that imports our tools:** `installTo(server)` needs a v2 `McpServer`; a v1 server from `@modelcontextprotocol/sdk` won't work. The hosted server builds its own server, so it has to move to v2 in the same release as this and mcp-server. Tool and resource handlers now get the v2 context, so the access token is at `ctx.http.authInfo.token` instead of `extra.authInfo.token`. - **Clients:** nothing changes. It still negotiates 2025-11-25 with current clients, and the MCP Apps wire protocol is the same in ext-apps 2.x, so the preview and comparison UIs work as before. - **No more SDK patch:** the v1 SDK was patched so a tool's `structuredContent` that didn't match its output schema, or was missing, logged a warning instead of failing the call. v2 takes any Standard Schema as an output schema, so `lenientOutputSchema` wraps each tool's Zod schema: `tools/list` publishes the real JSON Schema (now 2020-12, from v2) and validation only warns. A tool with an output schema that returns only text gets `structuredContent: {}` instead of an error. `patches/` is now empty. From ec7c4c33e6f820fb2c70cae4b5a781e8e8ae8aa7 Mon Sep 17 00:00:00 2001 From: Matthew Podwysocki Date: Fri, 23 Oct 2026 10:20:40 -0400 Subject: [PATCH 3/4] test: fix all test type errors and typecheck tests in CI Adds npm run typecheck (tsc over src and test) as a CI step. Fixes the 247 errors it found: content/resource/prompt text reads go through helpers that narrow the block type, StyleBuilderTool tests use the schema's input type, and a bare src/ import is made relative. Sets noEmit in tsconfig.test.json. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/test.yml | 3 + CHANGELOG.md | 4 ++ package.json | 1 + test/prompts/AnalyzeGeojsonPrompt.test.ts | 13 ++-- test/prompts/BuildCustomMapPrompt.test.ts | 9 +-- .../CreateAndPreviewStylePrompt.test.ts | 9 +-- .../DebugMapboxIntegrationPrompt.test.ts | 19 +++--- .../DesignDataDrivenStylePrompt.test.ts | 27 ++++---- .../PrepareStyleForProductionPrompt.test.ts | 21 +++--- test/prompts/SetupMapboxProjectPrompt.test.ts | 15 +++-- test/resources/resourceRegistry.test.ts | 11 ++- .../ui-apps/MapPreviewUIResource.test.ts | 9 +-- test/security/path-traversal.test.ts | 3 +- .../CheckColorContrastTool.test.ts | 51 +++++++------- .../CompareStylesTool.test.ts | 29 ++++---- .../create-token-tool/CreateTokenTool.test.ts | 2 +- .../get-feedback-tool/GetFeedbackTool.test.ts | 23 ++++--- .../ListFeedbackTool.test.ts | 29 ++++---- .../OptimizeStyleTool.test.ts | 39 +++++------ .../StyleBuilderTool.test.ts | 67 ++++++++++--------- .../ValidateExpressionTool.test.ts | 57 ++++++++-------- .../ValidateGeojsonTool.test.ts | 53 ++++++++------- .../ValidateStyleTool.test.ts | 29 ++++---- test/utils/resultTestUtils.ts | 46 +++++++++++++ tsconfig.test.json | 1 + 25 files changed, 324 insertions(+), 246 deletions(-) create mode 100644 test/utils/resultTestUtils.ts diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 34ffed9..6b42c9a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -29,6 +29,9 @@ jobs: - name: Check formatting run: npm run format + - name: Typecheck src and tests + run: npm run typecheck + - name: Compile typescript run: npm run build diff --git a/CHANGELOG.md b/CHANGELOG.md index abe153a..8b8e855 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,10 @@ - `style_comparison_tool`'s `StyleComparisonUIResource` is untouched and stays separate — it renders two synced `mapboxgl.Map` instances under a swipe/compare slider, a genuinely different UI shape from "one map, different content," not just another mode to fold in. - The GeoJSON-preview path keeps its existing eager map bootstrap (drawing the default Standard style immediately, before any tool result arrives, so an overlay has something to render onto right away); the style-preview path now reuses that same map instance via `setStyle()` when it exists, falling back to constructing its own (as before) when it doesn't. Verified live against the real Mapbox API in a real browser: the GeoJSON overlay path and the style-swap-in-place path (loading a real custom style by name) both work end-to-end on the merged resource. +### Maintenance + +- **Tests are typechecked in CI (#TBD).** `npm run typecheck` checks `src` and `test` together and now runs before the build. The tests had 247 type errors, all already on main: 238 read `.text` off a content block without checking it was text (now `textOf`, `resourceTextOf` and `promptTextOf` in `test/utils/resultTestUtils.ts`, which fail clearly on the wrong content type), the `StyleBuilderTool` tests typed raw input with the parsed type that makes `render_type` required, and one import used a bare `src/` path. `tsconfig.test.json` gets `noEmit` so a plain `tsc` run can't write `.js` files next to the sources. + ### Dependencies - Bumped `@modelcontextprotocol/sdk` to `1.30.0`. Not adopting the `2026-07-28` spec revision this release covers (stateless request/response model, elicitation replaced by Multi Round-Trip Requests, Sampling deprecated) — that's a separate migration, tracked in #130, given this repo's own elicitation-based features depend on the mechanism being replaced. Regenerated `patches/@modelcontextprotocol+sdk+1.30.0.patch` (previously pinned to `1.29.0`) — same patch content, applies cleanly to the new version, verified live against the built server. diff --git a/package.json b/package.json index 3034649..7df87ec 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "spellcheck": "cspell \"*.md\" \"src/**/*.ts\" \"test/**/*.ts\" \"examples/**/*.ts\"", "sync-manifest": "node scripts/sync-manifest-version.cjs", "test": "vitest", + "typecheck": "tsc -p tsconfig.test.json", "tracing:jaeger:start": "docker run --rm -d --name jaeger -p 16686:16686 -p 14250:14250 -p 4317:4317 -p 4318:4318 jaegertracing/all-in-one:latest", "tracing:jaeger:stop": "docker stop jaeger", "tracing:verify": "node -e \"console.log('🔍 Verifying tracing setup with Jaeger...\\n1. Copy .env.example to .env and add your MAPBOX_ACCESS_TOKEN\\n2. Start Jaeger: npm run tracing:jaeger:start\\n3. Run server: npm run inspect:build\\n4. Check traces at: http://localhost:16686')\"" diff --git a/test/prompts/AnalyzeGeojsonPrompt.test.ts b/test/prompts/AnalyzeGeojsonPrompt.test.ts index 1fd9928..8088889 100644 --- a/test/prompts/AnalyzeGeojsonPrompt.test.ts +++ b/test/prompts/AnalyzeGeojsonPrompt.test.ts @@ -3,6 +3,7 @@ import { describe, it, expect } from 'vitest'; import { AnalyzeGeojsonPrompt } from '../../src/prompts/AnalyzeGeojsonPrompt.js'; +import { promptTextOf } from '../utils/resultTestUtils.js'; describe('AnalyzeGeojsonPrompt', () => { const prompt = new AnalyzeGeojsonPrompt(); @@ -50,7 +51,7 @@ describe('AnalyzeGeojsonPrompt', () => { expect(result.messages[0].role).toBe('user'); expect(result.messages[0].content.type).toBe('text'); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('Analyze the provided GeoJSON'); expect(text).toContain('bounding_box_tool'); expect(text).toContain('geojson_preview_tool'); @@ -61,7 +62,7 @@ describe('AnalyzeGeojsonPrompt', () => { geojson_data: sampleGeojson }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('Calculate bounding box'); expect(text).toContain('bounding_box_tool'); }); @@ -72,7 +73,7 @@ describe('AnalyzeGeojsonPrompt', () => { show_bounds: 'false' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); // Should still mention visualization but with different step number expect(text).toContain('Generate visualization'); }); @@ -83,7 +84,7 @@ describe('AnalyzeGeojsonPrompt', () => { convert_coordinates: 'true' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('coordinate_conversion_tool'); expect(text).toContain('Web Mercator'); expect(text).toContain('EPSG:3857'); @@ -94,7 +95,7 @@ describe('AnalyzeGeojsonPrompt', () => { geojson_data: sampleGeojson }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain(sampleGeojson); }); @@ -109,7 +110,7 @@ describe('AnalyzeGeojsonPrompt', () => { geojson_data: sampleGeojson }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('Analysis guidelines'); expect(text).toContain('longitude, latitude'); expect(text).toContain('right-hand rule'); diff --git a/test/prompts/BuildCustomMapPrompt.test.ts b/test/prompts/BuildCustomMapPrompt.test.ts index e2489f1..9299622 100644 --- a/test/prompts/BuildCustomMapPrompt.test.ts +++ b/test/prompts/BuildCustomMapPrompt.test.ts @@ -3,6 +3,7 @@ import { describe, it, expect } from 'vitest'; import { BuildCustomMapPrompt } from '../../src/prompts/BuildCustomMapPrompt.js'; +import { promptTextOf } from '../utils/resultTestUtils.js'; describe('BuildCustomMapPrompt', () => { const prompt = new BuildCustomMapPrompt(); @@ -37,7 +38,7 @@ describe('BuildCustomMapPrompt', () => { expect(result.messages[0].role).toBe('user'); expect(result.messages[0].content.type).toBe('text'); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('dark cyberpunk'); expect(text).toContain('style_builder_tool'); expect(text).toContain('create_style_tool'); @@ -50,7 +51,7 @@ describe('BuildCustomMapPrompt', () => { emphasis: 'parks and green spaces' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('parks and green spaces'); expect(text).toContain('emphasizing'); }); @@ -62,7 +63,7 @@ describe('BuildCustomMapPrompt', () => { preview_zoom: '15' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('Tokyo'); expect(text).toContain('15'); }); @@ -78,7 +79,7 @@ describe('BuildCustomMapPrompt', () => { theme: 'retro 80s neon' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('Theme interpretation tips'); expect(text).toContain('Dark cyberpunk'); expect(text).toContain('Nature-focused'); diff --git a/test/prompts/CreateAndPreviewStylePrompt.test.ts b/test/prompts/CreateAndPreviewStylePrompt.test.ts index 924a7d6..38f508b 100644 --- a/test/prompts/CreateAndPreviewStylePrompt.test.ts +++ b/test/prompts/CreateAndPreviewStylePrompt.test.ts @@ -3,6 +3,7 @@ import { describe, it, expect } from 'vitest'; import { CreateAndPreviewStylePrompt } from '../../src/prompts/CreateAndPreviewStylePrompt.js'; +import { promptTextOf } from '../utils/resultTestUtils.js'; describe('CreateAndPreviewStylePrompt', () => { const prompt = new CreateAndPreviewStylePrompt(); @@ -44,7 +45,7 @@ describe('CreateAndPreviewStylePrompt', () => { expect(result.messages[0].role).toBe('user'); expect(result.messages[0].content.type).toBe('text'); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('Test Style'); expect(text).toContain('list_tokens_tool'); expect(text).toContain('create_style_tool'); @@ -53,7 +54,7 @@ describe('CreateAndPreviewStylePrompt', () => { it('defaults base_style to "standard", not the Classic "streets-v12"', () => { const result = prompt.execute({ style_name: 'Test Style' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('base_style: "standard"'); expect(text).not.toContain('streets-v12'); @@ -65,7 +66,7 @@ describe('CreateAndPreviewStylePrompt', () => { style_name: 'Test Style', base_style: 'streets-v12' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('base_style: "streets-v12"'); }); @@ -79,7 +80,7 @@ describe('CreateAndPreviewStylePrompt', () => { preview_zoom: '14' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('A beautiful test style'); expect(text).toContain('dark-v11'); expect(text).toContain('San Francisco'); diff --git a/test/prompts/DebugMapboxIntegrationPrompt.test.ts b/test/prompts/DebugMapboxIntegrationPrompt.test.ts index 8bb6292..8e39e6f 100644 --- a/test/prompts/DebugMapboxIntegrationPrompt.test.ts +++ b/test/prompts/DebugMapboxIntegrationPrompt.test.ts @@ -3,6 +3,7 @@ import { describe, it, expect } from 'vitest'; import { DebugMapboxIntegrationPrompt } from '../../src/prompts/DebugMapboxIntegrationPrompt.js'; +import { promptTextOf } from '../utils/resultTestUtils.js'; describe('DebugMapboxIntegrationPrompt', () => { const prompt = new DebugMapboxIntegrationPrompt(); @@ -39,7 +40,7 @@ describe('DebugMapboxIntegrationPrompt', () => { expect(result.messages[0].role).toBe('user'); expect(result.messages[0].content.type).toBe('text'); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('Map not loading'); expect(text).toContain('list_tokens_tool'); expect(text).toContain('Phase 1'); @@ -51,7 +52,7 @@ describe('DebugMapboxIntegrationPrompt', () => { error_message: '401 Unauthorized' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('401 Unauthorized'); }); @@ -61,7 +62,7 @@ describe('DebugMapboxIntegrationPrompt', () => { error_message: '401 unauthorized' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('401 Unauthorized'); expect(text).toContain('Token not set'); expect(text).toContain('Invalid token'); @@ -73,7 +74,7 @@ describe('DebugMapboxIntegrationPrompt', () => { error_message: '403 Forbidden' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('403 Forbidden'); expect(text).toContain('URL restriction'); expect(text).toContain('Missing scope'); @@ -85,7 +86,7 @@ describe('DebugMapboxIntegrationPrompt', () => { style_id: 'my-style-id' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('my-style-id'); expect(text).toContain('list_styles_tool'); expect(text).toContain('Style Verification'); @@ -97,7 +98,7 @@ describe('DebugMapboxIntegrationPrompt', () => { environment: 'production' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('production'); }); @@ -107,7 +108,7 @@ describe('DebugMapboxIntegrationPrompt', () => { error_message: 'Layer source not found' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('Layer/Source Error Solutions'); }); @@ -116,7 +117,7 @@ describe('DebugMapboxIntegrationPrompt', () => { issue_description: 'Something is wrong' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('General debugging'); expect(text).toContain('browser console'); }); @@ -126,7 +127,7 @@ describe('DebugMapboxIntegrationPrompt', () => { issue_description: 'Issue' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('Phase 1'); expect(text).toContain('Phase 2'); expect(text).toContain('Phase 3'); diff --git a/test/prompts/DesignDataDrivenStylePrompt.test.ts b/test/prompts/DesignDataDrivenStylePrompt.test.ts index 6b7893a..f998bdc 100644 --- a/test/prompts/DesignDataDrivenStylePrompt.test.ts +++ b/test/prompts/DesignDataDrivenStylePrompt.test.ts @@ -3,6 +3,7 @@ import { describe, it, expect } from 'vitest'; import { DesignDataDrivenStylePrompt } from '../../src/prompts/DesignDataDrivenStylePrompt.js'; +import { promptTextOf } from '../utils/resultTestUtils.js'; describe('DesignDataDrivenStylePrompt', () => { const prompt = new DesignDataDrivenStylePrompt(); @@ -42,7 +43,7 @@ describe('DesignDataDrivenStylePrompt', () => { expect(result.messages[0].role).toBe('user'); expect(result.messages[0].content.type).toBe('text'); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('Population Map'); expect(text).toContain('City populations'); expect(text).toContain('population'); @@ -55,7 +56,7 @@ describe('DesignDataDrivenStylePrompt', () => { property_name: 'value' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('interpolate'); expect(text).toContain('["get", "value"]'); expect(text).toContain('Expression types'); @@ -69,7 +70,7 @@ describe('DesignDataDrivenStylePrompt', () => { visualization_type: 'color' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('fill-color'); expect(text).toContain('Color by Value'); }); @@ -82,7 +83,7 @@ describe('DesignDataDrivenStylePrompt', () => { visualization_type: 'size' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('circle-radius'); expect(text).toContain('Size by Value'); }); @@ -95,7 +96,7 @@ describe('DesignDataDrivenStylePrompt', () => { visualization_type: 'both' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('fill-color'); expect(text).toContain('circle-radius'); }); @@ -108,7 +109,7 @@ describe('DesignDataDrivenStylePrompt', () => { visualization_type: 'heatmap' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('heatmap'); expect(text).toContain('heatmap-weight'); expect(text).toContain('heatmap-color'); @@ -121,7 +122,7 @@ describe('DesignDataDrivenStylePrompt', () => { property_name: 'value' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('sequential'); }); @@ -133,7 +134,7 @@ describe('DesignDataDrivenStylePrompt', () => { color_scheme: 'diverging' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('Diverging'); expect(text).toContain('midpoint'); }); @@ -146,7 +147,7 @@ describe('DesignDataDrivenStylePrompt', () => { color_scheme: 'categorical' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('match'); expect(text).toContain('Category'); }); @@ -158,7 +159,7 @@ describe('DesignDataDrivenStylePrompt', () => { property_name: 'value' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('Step 1'); expect(text).toContain('Step 2'); expect(text).toContain('Step 3'); @@ -176,7 +177,7 @@ describe('DesignDataDrivenStylePrompt', () => { property_name: 'value' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('Advanced Expressions'); expect(text).toContain('Zoom-Based'); expect(text).toContain('Conditional'); @@ -189,7 +190,7 @@ describe('DesignDataDrivenStylePrompt', () => { property_name: 'value' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('Best Practices'); expect(text).toContain('DO:'); expect(text).toContain("DON'T:"); @@ -202,7 +203,7 @@ describe('DesignDataDrivenStylePrompt', () => { property_name: 'value' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('style_builder_tool'); expect(text).toContain('create_style_tool'); }); diff --git a/test/prompts/PrepareStyleForProductionPrompt.test.ts b/test/prompts/PrepareStyleForProductionPrompt.test.ts index ff5c1d9..6f6994a 100644 --- a/test/prompts/PrepareStyleForProductionPrompt.test.ts +++ b/test/prompts/PrepareStyleForProductionPrompt.test.ts @@ -3,6 +3,7 @@ import { describe, it, expect } from 'vitest'; import { PrepareStyleForProductionPrompt } from '../../src/prompts/PrepareStyleForProductionPrompt.js'; +import { promptTextOf } from '../utils/resultTestUtils.js'; describe('PrepareStyleForProductionPrompt', () => { const prompt = new PrepareStyleForProductionPrompt(); @@ -39,7 +40,7 @@ describe('PrepareStyleForProductionPrompt', () => { expect(result.messages[0].role).toBe('user'); expect(result.messages[0].content.type).toBe('text'); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('username/style-id'); expect(text).toContain('retrieve_style_tool'); expect(text).toContain('validate_expression_tool'); @@ -53,7 +54,7 @@ describe('PrepareStyleForProductionPrompt', () => { style_id_or_json: styleJson }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain(styleJson); expect(text).toContain('parse it directly'); expect(text).not.toContain('retrieve_style_tool'); @@ -64,7 +65,7 @@ describe('PrepareStyleForProductionPrompt', () => { style_id_or_json: 'test/style' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('validate_expression_tool'); expect(text).toContain('validate_geojson_tool'); expect(text).toContain('check_color_contrast_tool'); @@ -78,7 +79,7 @@ describe('PrepareStyleForProductionPrompt', () => { style_id_or_json: 'test/style' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('optimize_style_tool'); expect(text).toContain('Step 5: Optimize the Style'); expect(text).not.toContain('Skipped per user request'); @@ -90,7 +91,7 @@ describe('PrepareStyleForProductionPrompt', () => { skip_optimization: 'true' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('Step 5: Style Optimization'); expect(text).toContain('Skipped per user request'); expect(text).not.toContain('optimize_style_tool'); @@ -101,7 +102,7 @@ describe('PrepareStyleForProductionPrompt', () => { style_id_or_json: 'test/style' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('WCAG AA'); expect(text).toContain('level: "AA"'); }); @@ -112,7 +113,7 @@ describe('PrepareStyleForProductionPrompt', () => { wcag_level: 'AAA' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('WCAG AAA'); expect(text).toContain('level: "AAA"'); }); @@ -122,7 +123,7 @@ describe('PrepareStyleForProductionPrompt', () => { style_id_or_json: 'test/style' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('Production Quality Report'); expect(text).toContain('Expression Validation'); expect(text).toContain('GeoJSON Validation'); @@ -136,7 +137,7 @@ describe('PrepareStyleForProductionPrompt', () => { style_id_or_json: 'test/style' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('If Issues Are Found'); expect(text).toContain('Expression errors:'); expect(text).toContain('GeoJSON errors:'); @@ -161,7 +162,7 @@ describe('PrepareStyleForProductionPrompt', () => { style_id_or_json: 'test/style' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); const step1Index = text.indexOf('Step 1: Load the Style'); const step2Index = text.indexOf('Step 2: Validate All Expressions'); const step3Index = text.indexOf('Step 3: Validate GeoJSON Sources'); diff --git a/test/prompts/SetupMapboxProjectPrompt.test.ts b/test/prompts/SetupMapboxProjectPrompt.test.ts index 8857d79..64dee4a 100644 --- a/test/prompts/SetupMapboxProjectPrompt.test.ts +++ b/test/prompts/SetupMapboxProjectPrompt.test.ts @@ -3,6 +3,7 @@ import { describe, it, expect } from 'vitest'; import { SetupMapboxProjectPrompt } from '../../src/prompts/SetupMapboxProjectPrompt.js'; +import { promptTextOf } from '../utils/resultTestUtils.js'; describe('SetupMapboxProjectPrompt', () => { const prompt = new SetupMapboxProjectPrompt(); @@ -39,7 +40,7 @@ describe('SetupMapboxProjectPrompt', () => { expect(result.messages[0].role).toBe('user'); expect(result.messages[0].content.type).toBe('text'); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('MyApp'); expect(text).toContain('create_token_tool'); expect(text).toContain('style_builder_tool'); @@ -50,7 +51,7 @@ describe('SetupMapboxProjectPrompt', () => { project_name: 'MyApp' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('Development'); expect(text).toContain('localhost'); }); @@ -62,7 +63,7 @@ describe('SetupMapboxProjectPrompt', () => { production_domain: 'myapp.com' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('myapp.com'); expect(text).toContain('Production Token'); }); @@ -73,7 +74,7 @@ describe('SetupMapboxProjectPrompt', () => { project_type: 'web' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('No URL Restrictions'); expect(text).toContain('less secure'); }); @@ -84,7 +85,7 @@ describe('SetupMapboxProjectPrompt', () => { project_type: 'fullstack' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('Backend'); expect(text).toContain('SECRET'); expect(text).toContain('environment variables'); @@ -96,7 +97,7 @@ describe('SetupMapboxProjectPrompt', () => { project_type: 'mobile' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('Mobile'); expect(text).toContain('vision:read'); }); @@ -107,7 +108,7 @@ describe('SetupMapboxProjectPrompt', () => { style_theme: 'dark' }); - const text = result.messages[0].content.text; + const text = promptTextOf(result); expect(text).toContain('dark'); }); diff --git a/test/resources/resourceRegistry.test.ts b/test/resources/resourceRegistry.test.ts index 2867d1a..5f45020 100644 --- a/test/resources/resourceRegistry.test.ts +++ b/test/resources/resourceRegistry.test.ts @@ -4,13 +4,17 @@ import { describe, it, expect, vi, afterEach } from 'vitest'; import { Client } from '@modelcontextprotocol/client'; import { McpServer, InMemoryTransport } from '@modelcontextprotocol/server'; -import type { JSONRPCMessage } from '@modelcontextprotocol/server'; +import type { + JSONRPCMessage, + ServerContext +} from '@modelcontextprotocol/server'; import { RESOURCE_MIME_TYPE } from '@modelcontextprotocol/ext-apps/server'; import { getAllResources, installResources } from '../../src/resources/resourceRegistry.js'; import { MapPreviewUIResource } from '../../src/resources/ui-apps/MapPreviewUIResource.js'; +import { resourceTextOf } from '../utils/resultTestUtils.js'; function makeToken(prefix: 'sk' | 'pk' | 'tk', username: string): string { const payload = Buffer.from(JSON.stringify({ u: username })).toString( @@ -66,7 +70,8 @@ describe('installResources', () => { spies.forEach((spy) => { expect(spy).toHaveBeenCalledTimes(1); - expect(spy.mock.calls[0][1].http?.authInfo?.token).toBe('caller-token'); + const ctx = spy.mock.calls[0][1] as ServerContext; + expect(ctx.http?.authInfo?.token).toBe('caller-token'); }); await client.close(); }); @@ -104,7 +109,7 @@ describe('installResources', () => { ); const result = await client.readResource({ uri: resource.uri }); - const html = result.contents[0].text as string; + const html = resourceTextOf(result); const embedded = html.match(/var TOKEN = '([^']*)'/)?.[1] ?? ''; expect(fetchMock).toHaveBeenCalledTimes(1); diff --git a/test/resources/ui-apps/MapPreviewUIResource.test.ts b/test/resources/ui-apps/MapPreviewUIResource.test.ts index 4197773..1642dde 100644 --- a/test/resources/ui-apps/MapPreviewUIResource.test.ts +++ b/test/resources/ui-apps/MapPreviewUIResource.test.ts @@ -4,6 +4,7 @@ import { describe, it, expect, vi, afterEach } from 'vitest'; import * as vm from 'node:vm'; import { MapPreviewUIResource } from '../../../src/resources/ui-apps/MapPreviewUIResource.js'; +import { resourceTextOf } from '../../utils/resultTestUtils.js'; const uri = new URL('ui://mapbox/map-preview/index.html'); @@ -30,7 +31,7 @@ async function readHtml( token?: string ): Promise { const result = await resource['readCallback'](uri, extra(token)); - return result.contents[0].text as string; + return resourceTextOf(result); } // Stub global fetch to mirror real Mapbox behaviour: POST tokens/v2/{username} @@ -118,7 +119,7 @@ describe('MapPreviewUIResource — AGI-905 cross-account token leak', () => { ); expect(result.contents).toHaveLength(1); - expect(embeddedToken(result.contents[0].text as string)).toBe(''); + expect(embeddedToken(resourceTextOf(result))).toBe(''); }); it('passes a pk token through unchanged without minting', async () => { @@ -167,10 +168,10 @@ async function renderHtml(accessToken?: string): Promise { const resource = new MapPreviewUIResource(); if (accessToken?.startsWith('pk.')) { const result = await resource['readCallback'](uri, extra(accessToken)); - return result.contents[0].text as string; + return resourceTextOf(result); } const result = await resource['readCallback'](uri, extra()); - return result.contents[0].text as string; + return resourceTextOf(result); } function runScriptSandbox( diff --git a/test/security/path-traversal.test.ts b/test/security/path-traversal.test.ts index 75e0366..ae1960f 100644 --- a/test/security/path-traversal.test.ts +++ b/test/security/path-traversal.test.ts @@ -13,6 +13,7 @@ import { TilequeryTool } from '../../src/tools/tilequery-tool/TilequeryTool.js'; import { DeleteStyleTool } from '../../src/tools/delete-style-tool/DeleteStyleTool.js'; import { PreviewStyleTool } from '../../src/tools/preview-style-tool/PreviewStyleTool.js'; import { setupHttpRequest } from '../utils/httpPipelineUtils.js'; +import { textOf } from '../utils/resultTestUtils.js'; // 25-char alphanumeric — a real Mapbox style ID format const VALID_STYLE_ID = 'cmojrmkc9002t01ry96yi6h48'; @@ -206,7 +207,7 @@ describe('path traversal security', () => { accessToken: maliciousPublicToken }); - const url = result.content[0].text as string; + const url = textOf(result); expect(url).toContain('user%2Fattacker'); expect(url).not.toMatch(/\/styles\/v1\/user\/attacker\//); }); diff --git a/test/tools/check-color-contrast-tool/CheckColorContrastTool.test.ts b/test/tools/check-color-contrast-tool/CheckColorContrastTool.test.ts index deacd45..b3af067 100644 --- a/test/tools/check-color-contrast-tool/CheckColorContrastTool.test.ts +++ b/test/tools/check-color-contrast-tool/CheckColorContrastTool.test.ts @@ -3,6 +3,7 @@ import { describe, it, expect, beforeEach } from 'vitest'; import { CheckColorContrastTool } from '../../../src/tools/check-color-contrast-tool/CheckColorContrastTool.js'; +import { textOf } from '../../utils/resultTestUtils.js'; describe('CheckColorContrastTool', () => { let tool: CheckColorContrastTool; @@ -40,7 +41,7 @@ describe('CheckColorContrastTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.contrastRatio).toBe(21); expect(parsed.passes).toBe(true); expect(parsed.level).toBe('AA'); @@ -56,7 +57,7 @@ describe('CheckColorContrastTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.contrastRatio).toBe(21); expect(parsed.passes).toBe(true); }); @@ -74,7 +75,7 @@ describe('CheckColorContrastTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.contrastRatio).toBeGreaterThanOrEqual(4.5); expect(parsed.passes).toBe(true); expect(parsed.minimumRequired).toBe(4.5); @@ -91,7 +92,7 @@ describe('CheckColorContrastTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.contrastRatio).toBeGreaterThanOrEqual(3.0); expect(parsed.passes).toBe(true); expect(parsed.minimumRequired).toBe(3.0); @@ -108,7 +109,7 @@ describe('CheckColorContrastTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.contrastRatio).toBeLessThan(4.5); expect(parsed.passes).toBe(false); expect(parsed.recommendations).toBeDefined(); @@ -128,7 +129,7 @@ describe('CheckColorContrastTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.contrastRatio).toBeGreaterThanOrEqual(7.0); expect(parsed.passes).toBe(true); expect(parsed.minimumRequired).toBe(7.0); @@ -145,7 +146,7 @@ describe('CheckColorContrastTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.contrastRatio).toBeGreaterThanOrEqual(4.5); expect(parsed.passes).toBe(true); expect(parsed.minimumRequired).toBe(4.5); @@ -162,7 +163,7 @@ describe('CheckColorContrastTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.passes).toBe(false); expect(parsed.recommendations).toBeDefined(); // Should suggest it meets AA @@ -182,7 +183,7 @@ describe('CheckColorContrastTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.contrastRatio).toBe(21); }); @@ -195,7 +196,7 @@ describe('CheckColorContrastTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.contrastRatio).toBe(21); }); @@ -208,7 +209,7 @@ describe('CheckColorContrastTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.contrastRatio).toBe(21); }); @@ -221,7 +222,7 @@ describe('CheckColorContrastTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.contrastRatio).toBe(21); }); @@ -234,7 +235,7 @@ describe('CheckColorContrastTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.contrastRatio).toBe(21); }); @@ -247,7 +248,7 @@ describe('CheckColorContrastTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.contrastRatio).toBe(21); }); @@ -260,7 +261,7 @@ describe('CheckColorContrastTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.contrastRatio).toBe(21); }); }); @@ -274,7 +275,7 @@ describe('CheckColorContrastTool', () => { const result = await tool.run(input); expect(result.isError).toBe(true); - expect(result.content[0].text).toContain('Invalid foreground color'); + expect(textOf(result)).toContain('Invalid foreground color'); }); it('should handle invalid background color', async () => { @@ -285,7 +286,7 @@ describe('CheckColorContrastTool', () => { const result = await tool.run(input); expect(result.isError).toBe(true); - expect(result.content[0].text).toContain('Invalid background color'); + expect(textOf(result)).toContain('Invalid background color'); }); it('should handle malformed hex colors', async () => { @@ -309,7 +310,7 @@ describe('CheckColorContrastTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.level).toBe('AA'); }); @@ -322,7 +323,7 @@ describe('CheckColorContrastTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.fontSize).toBe('normal'); }); }); @@ -337,7 +338,7 @@ describe('CheckColorContrastTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.recommendations).toBeUndefined(); }); @@ -352,7 +353,7 @@ describe('CheckColorContrastTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.recommendations).toBeDefined(); expect(parsed.recommendations.length).toBeGreaterThan(0); expect( @@ -371,7 +372,7 @@ describe('CheckColorContrastTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); if (!parsed.passes) { expect( parsed.recommendations.some((r: string) => r.includes('large text')) @@ -390,7 +391,7 @@ describe('CheckColorContrastTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.wcagRequirements).toBeDefined(); expect(parsed.wcagRequirements.AA.normal).toBe(4.5); expect(parsed.wcagRequirements.AA.large).toBe(3.0); @@ -409,7 +410,7 @@ describe('CheckColorContrastTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.contrastRatio).toBeGreaterThan(1); }); @@ -422,7 +423,7 @@ describe('CheckColorContrastTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.contrastRatio).toBeGreaterThan(1); }); }); diff --git a/test/tools/compare-styles-tool/CompareStylesTool.test.ts b/test/tools/compare-styles-tool/CompareStylesTool.test.ts index 9eb58d8..74077aa 100644 --- a/test/tools/compare-styles-tool/CompareStylesTool.test.ts +++ b/test/tools/compare-styles-tool/CompareStylesTool.test.ts @@ -3,6 +3,7 @@ import { describe, it, expect, beforeEach } from 'vitest'; import { CompareStylesTool } from '../../../src/tools/compare-styles-tool/CompareStylesTool.js'; +import { textOf } from '../../utils/resultTestUtils.js'; describe('CompareStylesTool', () => { let tool: CompareStylesTool; @@ -47,7 +48,7 @@ describe('CompareStylesTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.identical).toBe(true); expect(parsed.differences).toHaveLength(0); expect(parsed.summary.totalDifferences).toBe(0); @@ -75,7 +76,7 @@ describe('CompareStylesTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.identical).toBe(false); expect(parsed.differences).toHaveLength(1); expect(parsed.differences[0].type).toBe('added'); @@ -103,7 +104,7 @@ describe('CompareStylesTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.identical).toBe(false); expect(parsed.differences).toHaveLength(1); expect(parsed.differences[0].type).toBe('removed'); @@ -130,7 +131,7 @@ describe('CompareStylesTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.identical).toBe(false); expect(parsed.differences).toHaveLength(1); expect(parsed.differences[0].type).toBe('modified'); @@ -169,7 +170,7 @@ describe('CompareStylesTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.identical).toBe(false); expect( parsed.differences.some((d: any) => d.path.includes('background')) @@ -202,7 +203,7 @@ describe('CompareStylesTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.identical).toBe(false); expect( parsed.differences.some( @@ -244,7 +245,7 @@ describe('CompareStylesTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.identical).toBe(false); expect(parsed.differences.some((d: any) => d.type === 'modified')).toBe( true @@ -283,7 +284,7 @@ describe('CompareStylesTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.identical).toBe(true); expect(parsed.differences).toHaveLength(0); }); @@ -311,7 +312,7 @@ describe('CompareStylesTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.identical).toBe(false); expect(parsed.differences).toHaveLength(1); expect(parsed.differences[0].path).toBe('id'); @@ -334,7 +335,7 @@ describe('CompareStylesTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.identical).toBe(true); }); }); @@ -348,7 +349,7 @@ describe('CompareStylesTool', () => { const result = await tool.run(input); expect(result.isError).toBe(true); - expect(result.content[0].text).toContain('Error parsing style A'); + expect(textOf(result)).toContain('Error parsing style A'); }); it('should handle invalid JSON for style B', async () => { @@ -359,7 +360,7 @@ describe('CompareStylesTool', () => { const result = await tool.run(input); expect(result.isError).toBe(true); - expect(result.content[0].text).toContain('Error parsing style B'); + expect(textOf(result)).toContain('Error parsing style B'); }); }); @@ -393,7 +394,7 @@ describe('CompareStylesTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.identical).toBe(false); expect( parsed.differences.some((d: any) => @@ -429,7 +430,7 @@ describe('CompareStylesTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.summary.totalDifferences).toBeGreaterThan(0); expect(parsed.summary.added).toBeGreaterThan(0); expect(parsed.summary.removed).toBeGreaterThan(0); diff --git a/test/tools/create-token-tool/CreateTokenTool.test.ts b/test/tools/create-token-tool/CreateTokenTool.test.ts index c3b64c8..fb73c87 100644 --- a/test/tools/create-token-tool/CreateTokenTool.test.ts +++ b/test/tools/create-token-tool/CreateTokenTool.test.ts @@ -8,7 +8,7 @@ import { } from '../../utils/httpPipelineUtils.js'; import { MapboxApiBasedTool } from '../../../src/tools/MapboxApiBasedTool.js'; import { CreateTokenTool } from '../../../src/tools/create-token-tool/CreateTokenTool.js'; -import { HttpRequest } from 'src/utils/types.js'; +import type { HttpRequest } from '../../../src/utils/types.js'; // Create a token with username in the payload const payload = Buffer.from(JSON.stringify({ u: 'testuser' })).toString( diff --git a/test/tools/get-feedback-tool/GetFeedbackTool.test.ts b/test/tools/get-feedback-tool/GetFeedbackTool.test.ts index 00ef244..06d2561 100644 --- a/test/tools/get-feedback-tool/GetFeedbackTool.test.ts +++ b/test/tools/get-feedback-tool/GetFeedbackTool.test.ts @@ -7,6 +7,7 @@ import { assertHeadersSent } from '../../utils/httpPipelineUtils.js'; import { GetFeedbackTool } from '../../../src/tools/get-feedback-tool/GetFeedbackTool.js'; +import { textOf } from '../../utils/resultTestUtils.js'; const mockToken = 'sk.eyJ1IjoidGVzdC11c2VyIiwiYSI6InRlc3QtYXBpIn0.signature'; @@ -125,10 +126,10 @@ describe('GetFeedbackTool', () => { expect(result.isError).toBe(false); expect(result.content[0].type).toBe('text'); - expect(result.content[0].text).toContain(mockItem.id); - expect(result.content[0].text).toContain(mockItem.status); - expect(result.content[0].text).toContain(mockItem.category); - expect(result.content[0].text).toContain(mockItem.feedback); + expect(textOf(result)).toContain(mockItem.id); + expect(textOf(result)).toContain(mockItem.status); + expect(textOf(result)).toContain(mockItem.category); + expect(textOf(result)).toContain(mockItem.feedback); expect(result.structuredContent).toEqual(mockItem); }); @@ -159,7 +160,7 @@ describe('GetFeedbackTool', () => { expect(result.isError).toBe(false); expect(result.content[0].type).toBe('text'); - const parsed = JSON.parse(result.content[0].text as string); + const parsed = JSON.parse(textOf(result)); expect(parsed).toEqual(mockItem); }); @@ -175,9 +176,9 @@ describe('GetFeedbackTool', () => { }); expect(result.isError).toBe(true); - expect(result.content[0].text).toContain('Failed to get feedback'); - expect(result.content[0].text).toContain('404'); - expect(result.content[0].text).toContain('Not Found'); + expect(textOf(result)).toContain('Failed to get feedback'); + expect(textOf(result)).toContain('404'); + expect(textOf(result)).toContain('Not Found'); assertHeadersSent(mockHttpRequest); }); @@ -229,7 +230,7 @@ describe('GetFeedbackTool', () => { // 1. Returning the data as-is (if format succeeds) // 2. Returning an error response (if formatting fails) // Both are acceptable graceful handling - expect(typeof result.content[0].text).toBe('string'); + expect(typeof textOf(result)).toBe('string'); }); it('validates feedback_id format', async () => { @@ -256,7 +257,7 @@ describe('GetFeedbackTool', () => { // Should fail validation expect(result.isError).toBe(true); - expect(result.content[0].text).toContain('invalid_type'); - expect(result.content[0].text).toContain('feedback_id'); + expect(textOf(result)).toContain('invalid_type'); + expect(textOf(result)).toContain('feedback_id'); }); }); diff --git a/test/tools/list-feedback-tool/ListFeedbackTool.test.ts b/test/tools/list-feedback-tool/ListFeedbackTool.test.ts index 9ddc803..c77198d 100644 --- a/test/tools/list-feedback-tool/ListFeedbackTool.test.ts +++ b/test/tools/list-feedback-tool/ListFeedbackTool.test.ts @@ -7,6 +7,7 @@ import { assertHeadersSent } from '../../utils/httpPipelineUtils.js'; import { ListFeedbackTool } from '../../../src/tools/list-feedback-tool/ListFeedbackTool.js'; +import { textOf } from '../../utils/resultTestUtils.js'; const mockToken = 'sk.eyJ1IjoidGVzdC11c2VyIiwiYSI6InRlc3QtYXBpIn0.signature'; @@ -164,13 +165,9 @@ describe('ListFeedbackTool', () => { expect(result.isError).toBe(false); expect(result.content[0].type).toBe('text'); - expect(result.content[0].text).toContain('Found 2 feedback item(s)'); - expect(result.content[0].text).toContain( - '40eae4c7-b157-4b49-a091-7e1099bba77e' - ); - expect(result.content[0].text).toContain( - '8b1eec47-a3f2-4a6d-a2d9-5e8c1f4a9b0c' - ); + expect(textOf(result)).toContain('Found 2 feedback item(s)'); + expect(textOf(result)).toContain('40eae4c7-b157-4b49-a091-7e1099bba77e'); + expect(textOf(result)).toContain('8b1eec47-a3f2-4a6d-a2d9-5e8c1f4a9b0c'); expect(result.structuredContent).toEqual(mockResponse); }); @@ -186,7 +183,7 @@ describe('ListFeedbackTool', () => { const result = await new ListFeedbackTool({ httpRequest }).run({}); expect(result.isError).toBe(false); - expect(result.content[0].text).toContain('No feedback items found'); + expect(textOf(result)).toContain('No feedback items found'); }); it('handles list response with pagination info', async () => { @@ -218,10 +215,10 @@ describe('ListFeedbackTool', () => { const result = await new ListFeedbackTool({ httpRequest }).run({}); expect(result.isError).toBe(false); - expect(result.content[0].text).toContain('Has previous page'); - expect(result.content[0].text).toContain('Has next page'); - expect(result.content[0].text).toContain('cursor1'); - expect(result.content[0].text).toContain('cursor2'); + expect(textOf(result)).toContain('Has previous page'); + expect(textOf(result)).toContain('Has next page'); + expect(textOf(result)).toContain('cursor1'); + expect(textOf(result)).toContain('cursor2'); }); it('returns JSON format when requested', async () => { @@ -256,7 +253,7 @@ describe('ListFeedbackTool', () => { expect(result.isError).toBe(false); expect(result.content[0].type).toBe('text'); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed).toEqual(mockResponse); }); @@ -270,9 +267,9 @@ describe('ListFeedbackTool', () => { const result = await new ListFeedbackTool({ httpRequest }).run({}); expect(result.isError).toBe(true); - expect(result.content[0].text).toContain('Failed to list feedback'); - expect(result.content[0].text).toContain('404'); - expect(result.content[0].text).toContain('Not Found'); + expect(textOf(result)).toContain('Failed to list feedback'); + expect(textOf(result)).toContain('404'); + expect(textOf(result)).toContain('Not Found'); assertHeadersSent(mockHttpRequest); }); diff --git a/test/tools/optimize-style-tool/OptimizeStyleTool.test.ts b/test/tools/optimize-style-tool/OptimizeStyleTool.test.ts index f9d7855..6b2be25 100644 --- a/test/tools/optimize-style-tool/OptimizeStyleTool.test.ts +++ b/test/tools/optimize-style-tool/OptimizeStyleTool.test.ts @@ -3,6 +3,7 @@ import { describe, it, expect, beforeEach } from 'vitest'; import { OptimizeStyleTool } from '../../../src/tools/optimize-style-tool/OptimizeStyleTool.js'; +import { textOf } from '../../utils/resultTestUtils.js'; describe('OptimizeStyleTool', () => { let tool: OptimizeStyleTool; @@ -41,7 +42,7 @@ describe('OptimizeStyleTool', () => { const result = await tool.run({ style }); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.optimizedStyle).toBeDefined(); expect(parsed.optimizations).toBeDefined(); expect(parsed.summary).toBeDefined(); @@ -57,7 +58,7 @@ describe('OptimizeStyleTool', () => { const result = await tool.run({ style: JSON.stringify(style) }); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.optimizedStyle).toBeDefined(); }); }); @@ -85,7 +86,7 @@ describe('OptimizeStyleTool', () => { }); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.optimizedStyle.sources).toHaveProperty('used-source'); expect(parsed.optimizedStyle.sources).not.toHaveProperty('unused-source'); expect(parsed.optimizations).toHaveLength(1); @@ -111,7 +112,7 @@ describe('OptimizeStyleTool', () => { }); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.optimizations).toHaveLength(0); }); }); @@ -146,7 +147,7 @@ describe('OptimizeStyleTool', () => { }); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.optimizedStyle.layers).toHaveLength(2); expect(parsed.optimizations[0].count).toBe(1); }); @@ -167,7 +168,7 @@ describe('OptimizeStyleTool', () => { }); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.optimizedStyle.layers).toHaveLength(2); expect(parsed.optimizations).toHaveLength(0); }); @@ -194,7 +195,7 @@ describe('OptimizeStyleTool', () => { }); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.optimizedStyle.layers[0].filter).toBe(true); expect(parsed.optimizations[0].count).toBeGreaterThan(0); }); @@ -219,7 +220,7 @@ describe('OptimizeStyleTool', () => { }); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.optimizedStyle.layers[0].filter).toBe(false); }); @@ -243,7 +244,7 @@ describe('OptimizeStyleTool', () => { }); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.optimizedStyle.layers[0].filter).toBe(true); }); @@ -267,7 +268,7 @@ describe('OptimizeStyleTool', () => { }); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.optimizedStyle.layers[0].filter).toBe(false); }); @@ -292,7 +293,7 @@ describe('OptimizeStyleTool', () => { }); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.optimizedStyle.layers[0].paint['fill-opacity']).toBe(true); }); }); @@ -323,7 +324,7 @@ describe('OptimizeStyleTool', () => { }); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.optimizedStyle.layers).toHaveLength(1); expect(parsed.optimizedStyle.layers[0].id).toBe('non-empty-layer'); expect(parsed.optimizations[0].count).toBe(1); @@ -349,7 +350,7 @@ describe('OptimizeStyleTool', () => { }); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.optimizedStyle.layers).toHaveLength(1); expect(parsed.optimizations).toHaveLength(0); }); @@ -382,7 +383,7 @@ describe('OptimizeStyleTool', () => { }); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); // Should identify the group of layers with identical filters expect(parsed.optimizations[0].count).toBeGreaterThan(0); }); @@ -423,7 +424,7 @@ describe('OptimizeStyleTool', () => { const result = await tool.run({ style }); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); // Should have removed unused source, duplicate layer, empty layer, and simplified expressions expect(parsed.optimizations.length).toBeGreaterThan(0); expect(parsed.summary.totalOptimizations).toBeGreaterThan(0); @@ -443,7 +444,7 @@ describe('OptimizeStyleTool', () => { const result = await tool.run({ style }); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.summary.originalSize).toBeGreaterThan(0); expect(parsed.summary.optimizedSize).toBeGreaterThan(0); expect(parsed.summary.sizeSaved).toBeGreaterThanOrEqual(0); @@ -464,7 +465,7 @@ describe('OptimizeStyleTool', () => { const result = await tool.run({ style }); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.summary.percentReduction).toBeGreaterThan(0); }); }); @@ -473,7 +474,7 @@ describe('OptimizeStyleTool', () => { it('should handle invalid JSON string', async () => { const result = await tool.run({ style: '{invalid json' }); expect(result.isError).toBe(true); - expect(result.content[0].text).toContain('Error parsing style'); + expect(textOf(result)).toContain('Error parsing style'); }); }); @@ -497,7 +498,7 @@ describe('OptimizeStyleTool', () => { const result = await tool.run({ style }); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.summary.totalOptimizations).toBe(0); expect(parsed.summary.sizeSaved).toBe(0); }); diff --git a/test/tools/style-builder-tool/StyleBuilderTool.test.ts b/test/tools/style-builder-tool/StyleBuilderTool.test.ts index 97ce8ca..d477ee3 100644 --- a/test/tools/style-builder-tool/StyleBuilderTool.test.ts +++ b/test/tools/style-builder-tool/StyleBuilderTool.test.ts @@ -3,7 +3,12 @@ import { describe, it, expect, beforeEach } from 'vitest'; import { StyleBuilderTool } from '../../../src/tools/style-builder-tool/StyleBuilderTool.js'; -import type { StyleBuilderToolInput } from '../../../src/tools/style-builder-tool/StyleBuilderTool.input.schema.js'; +import type { z } from 'zod'; +import type { StyleBuilderToolSchema } from '../../../src/tools/style-builder-tool/StyleBuilderTool.input.schema.js'; +import { textOf } from '../../utils/resultTestUtils.js'; + +// Raw tool input, before the schema fills in defaults like render_type. +type StyleBuilderToolInput = z.input; describe('StyleBuilderTool', () => { let tool: StyleBuilderTool; @@ -37,7 +42,7 @@ describe('StyleBuilderTool', () => { expect(result.isError).toBe(false); expect(result.content[0].type).toBe('text'); - const text = result.content[0].text as string; + const text = textOf(result); expect(text).toContain('Style Built Successfully'); expect(text).toContain('Test Style'); expect(text).toContain('"#0066ff"'); @@ -57,7 +62,7 @@ describe('StyleBuilderTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const text = result.content[0].text as string; + const text = textOf(result); expect(text).toContain('Mode:** dark'); expect(text).toContain('#000000'); }); @@ -80,7 +85,7 @@ describe('StyleBuilderTool', () => { }; const result = await tool.run(input); - const text = result.content[0].text as string; + const text = textOf(result); expect(result.isError).toBe(false); expect(text).toContain('#ff0000'); @@ -103,7 +108,7 @@ describe('StyleBuilderTool', () => { }; const result = await tool.run(input); - const text = result.content[0].text as string; + const text = textOf(result); expect(result.isError).toBe(false); expect(text).toContain('Highlighted'); @@ -124,7 +129,7 @@ describe('StyleBuilderTool', () => { }; const result = await tool.run(input); - const text = result.content[0].text as string; + const text = textOf(result); expect(result.isError).toBe(false); expect(text).toContain('Hidden'); @@ -144,7 +149,7 @@ describe('StyleBuilderTool', () => { }; const result = await tool.run(input); - const text = result.content[0].text as string; + const text = textOf(result); expect(result.isError).toBe(false); expect(text).toContain('Shown'); @@ -169,7 +174,7 @@ describe('StyleBuilderTool', () => { }; const result = await tool.run(input); - const text = result.content[0].text as string; + const text = textOf(result); expect(result.isError).toBe(false); @@ -211,7 +216,7 @@ describe('StyleBuilderTool', () => { }; const result = await tool.run(input); - const text = result.content[0].text as string; + const text = textOf(result); expect(result.isError).toBe(false); @@ -254,7 +259,7 @@ describe('StyleBuilderTool', () => { }; const result = await tool.run(input); - const text = result.content[0].text as string; + const text = textOf(result); const jsonMatch = text.match(/```json\n([\s\S]*?)\n```/); expect(jsonMatch).toBeTruthy(); @@ -290,7 +295,7 @@ describe('StyleBuilderTool', () => { }; const result = await tool.run(input); - const text = result.content[0].text as string; + const text = textOf(result); const jsonMatch = text.match(/```json\n([\s\S]*?)\n```/); const style = JSON.parse(jsonMatch![1]); @@ -322,7 +327,7 @@ describe('StyleBuilderTool', () => { // Should return help message, not error expect(result.isError).toBe(false); - const text = result.content[0].text as string; + const text = textOf(result); expect(text).toContain('not found'); expect(text).toContain('Available source layers'); }); @@ -345,7 +350,7 @@ describe('StyleBuilderTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const text = result.content[0].text as string; + const text = textOf(result); const jsonMatch = text.match(/```json\n([\s\S]*?)\n```/); const style = JSON.parse(jsonMatch![1]); @@ -380,7 +385,7 @@ describe('StyleBuilderTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const text = result.content[0].text as string; + const text = textOf(result); const jsonMatch = text.match(/```json\n([\s\S]*?)\n```/); const style = JSON.parse(jsonMatch![1]); @@ -419,7 +424,7 @@ describe('StyleBuilderTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const text = result.content[0].text as string; + const text = textOf(result); const jsonMatch = text.match(/```json\n([\s\S]*?)\n```/); const style = JSON.parse(jsonMatch![1]); @@ -459,7 +464,7 @@ describe('StyleBuilderTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const text = result.content[0].text as string; + const text = textOf(result); const jsonMatch = text.match(/```json\n([\s\S]*?)\n```/); const style = JSON.parse(jsonMatch![1]); @@ -493,7 +498,7 @@ describe('StyleBuilderTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const text = result.content[0].text as string; + const text = textOf(result); const jsonMatch = text.match(/```json\n([\s\S]*?)\n```/); const style = JSON.parse(jsonMatch![1]); @@ -534,7 +539,7 @@ describe('StyleBuilderTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const text = result.content[0].text as string; + const text = textOf(result); const styleJson = JSON.parse(text.match(/```json\n([\s\S]*?)\n```/)![1]); const transitLayer = styleJson.layers.find((l: any) => @@ -570,7 +575,7 @@ describe('StyleBuilderTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const text = result.content[0].text as string; + const text = textOf(result); const styleJson = JSON.parse(text.match(/```json\n([\s\S]*?)\n```/)![1]); const transitLayer = styleJson.layers.find((l: any) => @@ -608,7 +613,7 @@ describe('StyleBuilderTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const text = result.content[0].text as string; + const text = textOf(result); const styleJson = JSON.parse(text.match(/```json\n([\s\S]*?)\n```/)![1]); const roadsLayer = styleJson.layers.find((l: any) => @@ -640,7 +645,7 @@ describe('StyleBuilderTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const text = result.content[0].text as string; + const text = textOf(result); const jsonMatch = text.match(/```json\n([\s\S]*?)\n```/); const style = JSON.parse(jsonMatch![1]); @@ -673,7 +678,7 @@ describe('StyleBuilderTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const text = result.content[0].text as string; + const text = textOf(result); const jsonMatch = text.match(/```json\n([\s\S]*?)\n```/); const style = JSON.parse(jsonMatch![1]); @@ -709,7 +714,7 @@ describe('StyleBuilderTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const text = result.content[0].text as string; + const text = textOf(result); const jsonMatch = text.match(/```json\n([\s\S]*?)\n```/); const style = JSON.parse(jsonMatch![1]); @@ -740,7 +745,7 @@ describe('StyleBuilderTool', () => { }; const result = await tool.run(input); - const text = result.content[0].text as string; + const text = textOf(result); const jsonMatch = text.match(/```json\n([\s\S]*?)\n```/); const style = JSON.parse(jsonMatch![1]); @@ -803,7 +808,7 @@ describe('StyleBuilderTool', () => { }; const result = await tool.run(input); - const text = result.content[0].text as string; + const text = textOf(result); expect(text).toContain('Standard Config:** 15 properties set'); expect(text).toContain('Theme: faded'); @@ -845,7 +850,7 @@ describe('StyleBuilderTool', () => { }; const result = await tool.run(input); - const text = result.content[0].text as string; + const text = textOf(result); const jsonMatch = text.match(/```json\n([\s\S]*?)\n```/); const style = JSON.parse(jsonMatch![1]); @@ -897,7 +902,7 @@ describe('StyleBuilderTool', () => { }; const result = await tool.run(input); - const text = result.content[0].text as string; + const text = textOf(result); const jsonMatch = text.match(/```json\n([\s\S]*?)\n```/); const style = JSON.parse(jsonMatch![1]); @@ -938,7 +943,7 @@ describe('StyleBuilderTool', () => { }; const result = await tool.run(input); - const text = result.content[0].text as string; + const text = textOf(result); const jsonMatch = text.match(/```json\n([\s\S]*?)\n```/); const style = JSON.parse(jsonMatch![1]); @@ -979,7 +984,7 @@ describe('StyleBuilderTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const text = result.content[0].text as string; + const text = textOf(result); // No longer expecting auto-correction since we're using the correct layer expect(text).toContain('Style Built Successfully'); @@ -1022,7 +1027,7 @@ describe('StyleBuilderTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const text = result.content[0].text as string; + const text = textOf(result); expect(text).toContain( 'Determined source layer "poi_label" from filter properties' ); @@ -1064,7 +1069,7 @@ describe('StyleBuilderTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const text = result.content[0].text as string; + const text = textOf(result); expect(text).toContain('Layers Configured:** 4'); expect(text).toContain('Set to #0066ff'); diff --git a/test/tools/validate-expression-tool/ValidateExpressionTool.test.ts b/test/tools/validate-expression-tool/ValidateExpressionTool.test.ts index 1da6dd8..94cc527 100644 --- a/test/tools/validate-expression-tool/ValidateExpressionTool.test.ts +++ b/test/tools/validate-expression-tool/ValidateExpressionTool.test.ts @@ -3,6 +3,7 @@ import { describe, it, expect, beforeEach } from 'vitest'; import { ValidateExpressionTool } from '../../../src/tools/validate-expression-tool/ValidateExpressionTool.js'; +import { textOf } from '../../utils/resultTestUtils.js'; describe('ValidateExpressionTool', () => { let tool: ValidateExpressionTool; @@ -39,7 +40,7 @@ describe('ValidateExpressionTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(true); expect(parsed.errors).toHaveLength(0); expect(parsed.metadata.expressionType).toBe('literal'); @@ -53,7 +54,7 @@ describe('ValidateExpressionTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(true); expect(parsed.errors).toHaveLength(0); }); @@ -66,7 +67,7 @@ describe('ValidateExpressionTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(true); }); }); @@ -80,7 +81,7 @@ describe('ValidateExpressionTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(true); expect(parsed.errors).toHaveLength(0); expect(parsed.metadata.expressionType).toBe('get'); @@ -99,7 +100,7 @@ describe('ValidateExpressionTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(true); expect(parsed.errors).toHaveLength(0); }); @@ -120,7 +121,7 @@ describe('ValidateExpressionTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(true); expect(parsed.errors).toHaveLength(0); }); @@ -133,7 +134,7 @@ describe('ValidateExpressionTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(true); }); @@ -145,7 +146,7 @@ describe('ValidateExpressionTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(true); }); @@ -157,7 +158,7 @@ describe('ValidateExpressionTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(true); }); @@ -169,7 +170,7 @@ describe('ValidateExpressionTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(true); }); @@ -181,7 +182,7 @@ describe('ValidateExpressionTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(true); }); @@ -193,7 +194,7 @@ describe('ValidateExpressionTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(true); }); }); @@ -207,7 +208,7 @@ describe('ValidateExpressionTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(false); expect(parsed.errors[0].message).toContain( 'Expression array cannot be empty' @@ -222,7 +223,7 @@ describe('ValidateExpressionTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(false); expect(parsed.errors[0].message).toContain( 'Expression operator must be a string' @@ -237,7 +238,7 @@ describe('ValidateExpressionTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(false); expect(parsed.errors[0].message).toContain('Unknown expression operator'); }); @@ -250,7 +251,7 @@ describe('ValidateExpressionTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(false); expect(parsed.errors[0].message).toContain('Expected arguments of type'); }); @@ -263,7 +264,7 @@ describe('ValidateExpressionTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(false); expect(parsed.errors[0].message).toContain('Expected arguments of type'); }); @@ -289,7 +290,7 @@ describe('ValidateExpressionTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(true); expect(parsed.errors).toHaveLength(0); }); @@ -307,7 +308,7 @@ describe('ValidateExpressionTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(false); expect( parsed.errors.some((e: any) => @@ -331,7 +332,7 @@ describe('ValidateExpressionTool', () => { expect(Date.now() - start).toBeLessThan(1000); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(false); expect( parsed.errors.some((e: any) => @@ -353,7 +354,7 @@ describe('ValidateExpressionTool', () => { expect(Date.now() - start).toBeLessThan(1000); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(false); expect( parsed.errors.some((e: any) => @@ -368,7 +369,7 @@ describe('ValidateExpressionTool', () => { const result = await tool.run({ expression }); expect(result.isError).toBe(true); - expect(result.content[0].text).toContain('exceeds maximum length'); + expect(textOf(result)).toContain('exceeds maximum length'); }); }); @@ -385,7 +386,7 @@ describe('ValidateExpressionTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(true); expect(parsed.metadata.depth).toBeGreaterThan(0); }); @@ -402,7 +403,7 @@ describe('ValidateExpressionTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(true); expect(parsed.warnings.length).toBeGreaterThan(0); expect( @@ -425,7 +426,7 @@ describe('ValidateExpressionTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(true); }); }); @@ -438,7 +439,7 @@ describe('ValidateExpressionTool', () => { const result = await tool.run(input); expect(result.isError).toBe(true); - expect(result.content[0].text).toContain('Error parsing expression'); + expect(textOf(result)).toContain('Error parsing expression'); }); }); @@ -451,7 +452,7 @@ describe('ValidateExpressionTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.metadata).toBeDefined(); expect(parsed.metadata.expressionType).toBe('get'); expect(parsed.metadata.returnType).toBe('any'); @@ -466,7 +467,7 @@ describe('ValidateExpressionTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.metadata.depth).toBe(1); }); }); diff --git a/test/tools/validate-geojson-tool/ValidateGeojsonTool.test.ts b/test/tools/validate-geojson-tool/ValidateGeojsonTool.test.ts index b9970e9..08498fc 100644 --- a/test/tools/validate-geojson-tool/ValidateGeojsonTool.test.ts +++ b/test/tools/validate-geojson-tool/ValidateGeojsonTool.test.ts @@ -3,6 +3,7 @@ import { describe, it, expect, beforeEach } from 'vitest'; import { ValidateGeojsonTool } from '../../../src/tools/validate-geojson-tool/ValidateGeojsonTool.js'; +import { textOf } from '../../utils/resultTestUtils.js'; describe('ValidateGeojsonTool', () => { let tool: ValidateGeojsonTool; @@ -42,7 +43,7 @@ describe('ValidateGeojsonTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(true); expect(parsed.errors).toHaveLength(0); expect(parsed.statistics.type).toBe('Point'); @@ -66,7 +67,7 @@ describe('ValidateGeojsonTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(true); expect(parsed.errors).toHaveLength(0); expect(parsed.statistics.type).toBe('LineString'); @@ -91,7 +92,7 @@ describe('ValidateGeojsonTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(true); expect(parsed.errors).toHaveLength(0); expect(parsed.warnings).toHaveLength(0); @@ -115,7 +116,7 @@ describe('ValidateGeojsonTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(true); expect(parsed.errors).toHaveLength(0); expect(parsed.statistics.featureCount).toBe(1); @@ -152,7 +153,7 @@ describe('ValidateGeojsonTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(true); expect(parsed.errors).toHaveLength(0); expect(parsed.statistics.featureCount).toBe(2); @@ -183,7 +184,7 @@ describe('ValidateGeojsonTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(true); expect(parsed.errors).toHaveLength(0); }); @@ -199,7 +200,7 @@ describe('ValidateGeojsonTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(true); }); }); @@ -215,7 +216,7 @@ describe('ValidateGeojsonTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(false); expect(parsed.errors).toHaveLength(1); expect(parsed.errors[0].message).toContain( @@ -234,7 +235,7 @@ describe('ValidateGeojsonTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(false); expect(parsed.errors[0].message).toContain('Invalid GeoJSON type'); }); @@ -250,7 +251,7 @@ describe('ValidateGeojsonTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(false); expect(parsed.errors[0].message).toContain( 'Feature missing "geometry" property' @@ -271,7 +272,7 @@ describe('ValidateGeojsonTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(true); expect(parsed.warnings).toHaveLength(1); expect(parsed.warnings[0].message).toContain( @@ -289,7 +290,7 @@ describe('ValidateGeojsonTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(false); expect(parsed.errors[0].message).toContain( 'FeatureCollection missing "features" array' @@ -307,7 +308,7 @@ describe('ValidateGeojsonTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(true); expect(parsed.warnings).toHaveLength(1); expect(parsed.warnings[0].message).toContain( @@ -327,7 +328,7 @@ describe('ValidateGeojsonTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(false); expect(parsed.errors[0].message).toContain( 'missing "coordinates" property' @@ -345,7 +346,7 @@ describe('ValidateGeojsonTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(false); expect(parsed.errors[0].message).toContain('must be an array'); }); @@ -361,7 +362,7 @@ describe('ValidateGeojsonTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(false); expect(parsed.errors[0].message).toContain( 'must have at least 2 elements' @@ -379,7 +380,7 @@ describe('ValidateGeojsonTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(false); expect(parsed.errors[0].message).toContain('coordinates must be numbers'); }); @@ -395,7 +396,7 @@ describe('ValidateGeojsonTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(true); expect(parsed.warnings).toHaveLength(1); expect(parsed.warnings[0].message).toContain('Longitude'); @@ -413,7 +414,7 @@ describe('ValidateGeojsonTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(true); expect(parsed.warnings).toHaveLength(1); expect(parsed.warnings[0].message).toContain('Latitude'); @@ -431,7 +432,7 @@ describe('ValidateGeojsonTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(false); expect( parsed.errors.some((e: any) => @@ -457,7 +458,7 @@ describe('ValidateGeojsonTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(false); expect(parsed.errors[0].message).toContain( 'must have at least 4 positions' @@ -482,7 +483,7 @@ describe('ValidateGeojsonTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.valid).toBe(true); expect(parsed.warnings).toHaveLength(1); expect(parsed.warnings[0].message).toContain( @@ -499,7 +500,7 @@ describe('ValidateGeojsonTool', () => { const result = await tool.run(input); expect(result.isError).toBe(true); - expect(result.content[0].text).toContain('Error parsing GeoJSON'); + expect(textOf(result)).toContain('Error parsing GeoJSON'); }); }); @@ -519,7 +520,7 @@ describe('ValidateGeojsonTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.statistics.bbox).toEqual([99.0, -1.0, 101.0, 1.0]); }); @@ -550,7 +551,7 @@ describe('ValidateGeojsonTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.statistics.featureCount).toBe(3); }); @@ -598,7 +599,7 @@ describe('ValidateGeojsonTool', () => { const result = await tool.run(input); expect(result.isError).toBe(false); - const parsed = JSON.parse(result.content[0].text); + const parsed = JSON.parse(textOf(result)); expect(parsed.statistics.geometryTypes).toContain('Point'); expect(parsed.statistics.geometryTypes).toContain('LineString'); expect(parsed.statistics.geometryTypes).toContain('Polygon'); diff --git a/test/tools/validate-style-tool/ValidateStyleTool.test.ts b/test/tools/validate-style-tool/ValidateStyleTool.test.ts index 3920e7c..a1d4391 100644 --- a/test/tools/validate-style-tool/ValidateStyleTool.test.ts +++ b/test/tools/validate-style-tool/ValidateStyleTool.test.ts @@ -3,6 +3,7 @@ import { describe, it, expect, beforeEach } from 'vitest'; import { ValidateStyleTool } from '../../../src/tools/validate-style-tool/ValidateStyleTool.js'; +import { textOf } from '../../utils/resultTestUtils.js'; describe('ValidateStyleTool', () => { let tool: ValidateStyleTool; @@ -38,7 +39,7 @@ describe('ValidateStyleTool', () => { const result = await tool.run({ style: validStyle }); expect(result.isError).toBe(false); - const parsedResponse = JSON.parse(result.content[0].text); + const parsedResponse = JSON.parse(textOf(result)); expect(parsedResponse.valid).toBe(true); expect(parsedResponse.errors).toHaveLength(0); expect(parsedResponse.summary.layerCount).toBe(1); @@ -54,7 +55,7 @@ describe('ValidateStyleTool', () => { const result = await tool.run({ style: invalidStyle }); expect(result.isError).toBe(false); - const parsedResponse = JSON.parse(result.content[0].text); + const parsedResponse = JSON.parse(textOf(result)); expect(parsedResponse.valid).toBe(false); expect(parsedResponse.errors).toContainEqual( expect.objectContaining({ @@ -73,7 +74,7 @@ describe('ValidateStyleTool', () => { const result = await tool.run({ style: invalidStyle }); expect(result.isError).toBe(false); - const parsedResponse = JSON.parse(result.content[0].text); + const parsedResponse = JSON.parse(textOf(result)); expect(parsedResponse.valid).toBe(false); expect(parsedResponse.errors).toContainEqual( expect.objectContaining({ @@ -99,7 +100,7 @@ describe('ValidateStyleTool', () => { const result = await tool.run({ style: invalidStyle }); expect(result.isError).toBe(false); - const parsedResponse = JSON.parse(result.content[0].text); + const parsedResponse = JSON.parse(textOf(result)); expect(parsedResponse.valid).toBe(false); expect(parsedResponse.errors).toContainEqual( expect.objectContaining({ @@ -135,7 +136,7 @@ describe('ValidateStyleTool', () => { const result = await tool.run({ style: invalidStyle }); expect(result.isError).toBe(false); - const parsedResponse = JSON.parse(result.content[0].text); + const parsedResponse = JSON.parse(textOf(result)); expect(parsedResponse.valid).toBe(false); expect(parsedResponse.errors).toContainEqual( expect.objectContaining({ @@ -161,7 +162,7 @@ describe('ValidateStyleTool', () => { const result = await tool.run({ style: invalidStyle }); expect(result.isError).toBe(false); - const parsedResponse = JSON.parse(result.content[0].text); + const parsedResponse = JSON.parse(textOf(result)); expect(parsedResponse.valid).toBe(false); expect(parsedResponse.errors).toContainEqual( expect.objectContaining({ @@ -183,7 +184,7 @@ describe('ValidateStyleTool', () => { const result = await tool.run({ style: JSON.stringify(validStyle) }); expect(result.isError).toBe(false); - const parsedResponse = JSON.parse(result.content[0].text); + const parsedResponse = JSON.parse(textOf(result)); expect(parsedResponse).toHaveProperty('valid'); expect(parsedResponse).toHaveProperty('errors'); }); @@ -192,7 +193,7 @@ describe('ValidateStyleTool', () => { const result = await tool.run({ style: '{ invalid json }' }); expect(result.isError).toBe(true); - expect(result.content[0].text).toContain('Error parsing style JSON'); + expect(textOf(result)).toContain('Error parsing style JSON'); }); it('should provide warnings for missing sprite and glyphs', async () => { @@ -205,7 +206,7 @@ describe('ValidateStyleTool', () => { const result = await tool.run({ style }); expect(result.isError).toBe(false); - const parsedResponse = JSON.parse(result.content[0].text); + const parsedResponse = JSON.parse(textOf(result)); expect(parsedResponse.info).toContainEqual( expect.objectContaining({ severity: 'info', @@ -241,7 +242,7 @@ describe('ValidateStyleTool', () => { const result = await tool.run({ style }); expect(result.isError).toBe(false); - const parsedResponse = JSON.parse(result.content[0].text); + const parsedResponse = JSON.parse(textOf(result)); expect(parsedResponse.summary).toEqual({ version: 8, layerCount: 2, @@ -281,7 +282,7 @@ describe('ValidateStyleTool', () => { const result = await tool.run({ style }); expect(result.isError).toBe(false); - const parsedResponse = JSON.parse(result.content[0].text); + const parsedResponse = JSON.parse(textOf(result)); expect(parsedResponse.valid).toBe(false); expect( parsedResponse.errors.some((e: any) => @@ -326,7 +327,7 @@ describe('ValidateStyleTool', () => { const result = await tool.run({ style }); expect(result.isError).toBe(false); - const parsedResponse = JSON.parse(result.content[0].text); + const parsedResponse = JSON.parse(textOf(result)); expect(parsedResponse.valid).toBe(true); expect(parsedResponse.errors).toHaveLength(0); }); @@ -355,7 +356,7 @@ describe('ValidateStyleTool', () => { expect(Date.now() - start).toBeLessThan(1000); expect(result.isError).toBe(true); - expect(result.content[0].text).toContain('exceeds maximum nesting depth'); + expect(textOf(result)).toContain('exceeds maximum nesting depth'); }); it('should reject a style with a wide (shallow but huge) expression instead of exhausting memory', async () => { @@ -382,7 +383,7 @@ describe('ValidateStyleTool', () => { expect(Date.now() - start).toBeLessThan(1000); expect(result.isError).toBe(true); - expect(result.content[0].text).toContain('exceeding maximum size'); + expect(textOf(result)).toContain('exceeding maximum size'); }); it('should reject a JSON string style exceeding the maximum length', async () => { diff --git a/test/utils/resultTestUtils.ts b/test/utils/resultTestUtils.ts new file mode 100644 index 0000000..f00506b --- /dev/null +++ b/test/utils/resultTestUtils.ts @@ -0,0 +1,46 @@ +// Copyright (c) Mapbox, Inc. +// Licensed under the MIT License. + +import type { + CallToolResult, + GetPromptResult, + ReadResourceResult +} from '@modelcontextprotocol/server'; + +/** + * The text of a tool result's content block. Throws if that block isn't + * text, so a test fails on the wrong content type rather than on undefined. + */ +export function textOf(result: CallToolResult, index = 0): string { + const block = result.content[index]; + if (block?.type !== 'text') { + throw new Error( + `content[${index}] is ${block?.type ?? 'missing'}, not text` + ); + } + return block.text; +} + +/** + * The text of a resource read's contents entry. Throws if it's a blob. + */ +export function resourceTextOf(result: ReadResourceResult, index = 0): string { + const entry = result.contents[index]; + if (!entry || !('text' in entry)) { + throw new Error(`contents[${index}] is not text`); + } + return entry.text; +} + +/** + * The text of a prompt message. Throws if the message content isn't text. + */ +export function promptTextOf(result: GetPromptResult, index = 0): string { + const content = result.messages[index]?.content; + if (content?.type !== 'text') { + throw new Error( + `messages[${index}] is ${content?.type ?? 'missing'}, not text` + ); + } + return content.text; +} diff --git a/tsconfig.test.json b/tsconfig.test.json index 64c3b90..88cfbab 100644 --- a/tsconfig.test.json +++ b/tsconfig.test.json @@ -2,6 +2,7 @@ "extends": "./tsconfig.base.json", "compilerOptions": { "skipLibCheck": true, + "noEmit": true, "lib": ["ESNext"], "resolveJsonModule": true }, From be0cb528f42ff5b27d25e63e0e428a2c5cab3a5b Mon Sep 17 00:00:00 2001 From: Matthew Podwysocki Date: Fri, 23 Oct 2026 10:21:03 -0400 Subject: [PATCH 4/4] docs: add PR number to CHANGELOG Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b8e855..1a582da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,7 +34,7 @@ ### Maintenance -- **Tests are typechecked in CI (#TBD).** `npm run typecheck` checks `src` and `test` together and now runs before the build. The tests had 247 type errors, all already on main: 238 read `.text` off a content block without checking it was text (now `textOf`, `resourceTextOf` and `promptTextOf` in `test/utils/resultTestUtils.ts`, which fail clearly on the wrong content type), the `StyleBuilderTool` tests typed raw input with the parsed type that makes `render_type` required, and one import used a bare `src/` path. `tsconfig.test.json` gets `noEmit` so a plain `tsc` run can't write `.js` files next to the sources. +- **Tests are typechecked in CI (#143).** `npm run typecheck` checks `src` and `test` together and now runs before the build. The tests had 247 type errors, all already on main: 238 read `.text` off a content block without checking it was text (now `textOf`, `resourceTextOf` and `promptTextOf` in `test/utils/resultTestUtils.ts`, which fail clearly on the wrong content type), the `StyleBuilderTool` tests typed raw input with the parsed type that makes `render_type` required, and one import used a bare `src/` path. `tsconfig.test.json` gets `noEmit` so a plain `tsc` run can't write `.js` files next to the sources. ### Dependencies