diff --git a/CHANGELOG.md b/CHANGELOG.md index 17d822e..abe153a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,15 @@ ### Breaking Changes +- **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. + - **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(); + }); +});