From 2838aeff932cf9bbd35bfb8dd3c157ac4896698e Mon Sep 17 00:00:00 2001 From: Matthew Podwysocki Date: Wed, 21 Oct 2026 15:18:56 -0400 Subject: [PATCH 1/4] feat!: move to MCP TypeScript SDK v2 (#245) Swaps @modelcontextprotocol/sdk 1.x for the v2 packages (@modelcontextprotocol/server and /core, plus /client and /node for tests and scripts). 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 {}. - Make publishJsonSchema2020 a deprecated no-op, since v2 publishes JSON Schema 2020-12 natively. Kept for hosted's adapter. - Remove OptimizationV2Tool, which was never registered and used the experimental tasks API that v2 drops. - Move test request contexts to the v2 shape (http.authInfo) so the token checks actually run. - Replace the patch tests, which never went through the SDK, with tests that make a real tools/call. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 11 + docs/importing-tools.md | 20 +- evals/tool-selection/harness.ts | 5 +- examples/import-example.ts | 11 +- package-lock.json | 861 ++---------------- package.json | 7 +- .../@modelcontextprotocol+sdk+1.30.0.patch | 66 -- patches/README.md | 44 +- scripts/dev-http-server.ts | 6 +- src/index.ts | 24 +- src/prompts/BasePrompt.ts | 10 +- src/prompts/FindPlacesNearbyPrompt.ts | 2 +- src/prompts/GetDirectionsPrompt.ts | 2 +- src/prompts/SearchAlongRoutePrompt.ts | 2 +- src/prompts/ShowReachableAreasPrompt.ts | 2 +- src/resources/BaseResource.ts | 27 +- src/resources/MapboxApiBasedResource.ts | 15 +- .../category-list/CategoryListResource.ts | 2 +- src/resources/compute/ComputeResource.ts | 8 +- .../inline-payload/InlinePayloadResource.ts | 8 +- .../place-photo/PlacePhotoResource.ts | 2 +- .../temporary/TemporaryDataResource.ts | 13 +- .../tool-response/ToolResponseResource.ts | 2 +- src/resources/ui-apps/MapAppUIResource.ts | 13 +- src/resources/version/VersionResource.ts | 2 +- src/tools/BaseTool.ts | 43 +- src/tools/MapboxApiBasedTool.ts | 16 +- src/tools/area-tool/AreaTool.ts | 2 +- src/tools/bearing-tool/BearingTool.ts | 2 +- .../bounding-box-tool/BoundingBoxTool.ts | 2 +- src/tools/buffer-tool/BufferTool.ts | 2 +- .../category-list-tool/CategoryListTool.ts | 2 +- .../CategorySearchTool.ts | 2 +- src/tools/centroid-tool/CentroidTool.ts | 2 +- src/tools/convex-tool/ConvexTool.ts | 2 +- src/tools/destination-tool/DestinationTool.ts | 2 +- src/tools/difference-tool/DifferenceTool.ts | 2 +- src/tools/directions-tool/DirectionsTool.ts | 2 +- src/tools/distance-tool/DistanceTool.ts | 2 +- src/tools/feedback-tool/FeedbackTool.ts | 2 +- .../GroundLocationTool.ts | 2 +- src/tools/intersect-tool/IntersectTool.ts | 2 +- src/tools/isochrone-tool/IsochroneTool.ts | 2 +- src/tools/length-tool/LengthTool.ts | 2 +- src/tools/lenientOutputSchema.ts | 41 + .../map-matching-tool/MapMatchingTool.ts | 2 +- src/tools/matrix-tool/MatrixTool.ts | 2 +- src/tools/midpoint-tool/MidpointTool.ts | 2 +- .../NearestPointOnLineTool.ts | 2 +- .../nearest-point-tool/NearestPointTool.ts | 2 +- .../optimization-tool/OptimizationTool.ts | 2 +- .../OptimizationV2Tool.input.schema.ts | 215 ----- .../OptimizationV2Tool.output.schema.ts | 139 --- .../optimization-tool/OptimizationV2Tool.ts | 442 --------- .../place-details-tool/PlaceDetailsTool.ts | 2 +- .../PointsWithinPolygonTool.ts | 2 +- src/tools/render-map-tool/RenderMapTool.ts | 12 +- .../ResourceReaderTool.ts | 14 +- .../ReverseGeocodeTool.ts | 2 +- .../SearchAndGeocodeTool.ts | 2 +- src/tools/simplify-tool/SimplifyTool.ts | 2 +- .../StaticMapImageTool.ts | 2 +- src/tools/union-tool/UnionTool.ts | 2 +- src/tools/validateOutput.ts | 2 +- src/utils/index.ts | 4 +- src/utils/jsonSchema2020.ts | 51 +- src/utils/versionUtils-cjs.cjs | 92 +- src/utils/versionUtils-cjs.d.cts | 12 +- test/exports.test.ts | 8 +- .../hostedMultiTaskFollowup.test.ts | 4 +- test/integration/processRestart.test.ts | 4 +- test/patches/mcp-sdk-patch.test.ts | 282 ------ test/prompts/BasePrompt.test.ts | 2 +- test/resources/BaseResource.test.ts | 2 +- .../resources/compute/ComputeResource.test.ts | 2 +- .../temporary/TemporaryDataResource.test.ts | 2 +- .../ui-apps/MapAppUIResource.test.ts | 4 +- test/tools/BaseTool.test.ts | 2 +- test/tools/annotations.test.ts | 2 +- .../CategorySearchTool.test.ts | 2 +- .../difference-tool/DifferenceTool.test.ts | 2 +- .../directions-tool/DirectionsTool.test.ts | 6 +- .../GroundLocationTool.test.ts | 2 +- .../intersect-tool/IntersectTool.test.ts | 2 +- .../isochrone-tool/IsochroneTool.test.ts | 2 +- test/tools/lenientOutputSchema.test.ts | 130 +++ .../map-matching-tool/MapMatchingTool.test.ts | 2 +- .../OptimizationTool.test.ts | 2 +- .../render-map-tool/RenderMapTool.test.ts | 20 +- .../SearchAndGeocodeTool.test.ts | 2 +- test/tools/union-tool/UnionTool.test.ts | 2 +- test/utils/jsonSchema2020.test.ts | 27 +- 92 files changed, 546 insertions(+), 2277 deletions(-) delete mode 100644 patches/@modelcontextprotocol+sdk+1.30.0.patch create mode 100644 src/tools/lenientOutputSchema.ts delete mode 100644 src/tools/optimization-tool/OptimizationV2Tool.input.schema.ts delete mode 100644 src/tools/optimization-tool/OptimizationV2Tool.output.schema.ts delete mode 100644 src/tools/optimization-tool/OptimizationV2Tool.ts delete mode 100644 test/patches/mcp-sdk-patch.test.ts create mode 100644 test/tools/lenientOutputSchema.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 1541150d..19f28c00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ ## Unreleased +### Breaking Changes + +- **MCP TypeScript SDK v2 (#245).** The server and the `@mapbox/mcp-server/tools`, `/resources` and `/utils` 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 the line that gets new MCP features, such as the SDK's Skills extension support, and ext-apps 2.x requires it. + - **For code that imports our tools:** `installTo(server)` needs a v2 `McpServer`. A v1 server from `@modelcontextprotocol/sdk` won't work, so a deployment that builds its own server (the hosted server, for one) has to move to v2 in the same release. Tool and resource handlers now receive the v2 context: the access token is at `ctx.http.authInfo.token` instead of `extra.authInfo.token`. + - **Clients:** nothing changes. The server still answers the `initialize` handshake and negotiates 2025-11-25 with current clients, and the MCP Apps wire protocol is the same in ext-apps 2.x, so `render_map_tool` renders as before. Adopting the 2026-07-28 protocol revision (MRTR in place of elicitation, the Sampling replacement) is separate work under #245. + - **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` still publishes the real JSON Schema, and validation only warns. A tool with an output schema that returns only text gets `structuredContent: {}` instead of an error, since v2 checks for it before validating. `patches/` is now empty. + - **JSON Schema 2020-12:** v2 publishes tool schemas as 2020-12 itself, so `publishJsonSchema2020(server)` is now a deprecated no-op, kept so existing callers don't break. `toJsonSchema2020` is unchanged. + - **Removed:** `OptimizationV2Tool`, which was never registered and was built on the SDK's experimental tasks API, which v2 removes. + - **Codemod:** most of the change is `npx @modelcontextprotocol/codemod v1-to-v2 .` (import paths, method-string `setRequestHandler`, `RequestHandlerExtra` to `ServerContext`, `extra.authInfo` to `ctx.http?.authInfo`). `@modelcontextprotocol/client` and `@modelcontextprotocol/node` are dev dependencies, used only by tests and scripts. + - **Testing:** the old patch tests called `tool.run()` directly and never went through the SDK; they're replaced by tests that make a real `tools/call` through `McpServer` for a mismatched, a missing and a matching `structuredContent`, and they 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; they now use `{ http: { authInfo } }`, which also makes the map app's secret-token checks test what they say. A stdio smoke test against the built server covers the handshake, all 30 tools with 2020-12 schemas, prompts, completions, resources, the MCP Apps UI resource, logging and a tool call. + ### New Features - **`feedback_tool`: report wrong map, place or routing data to Mapbox (#291).** When someone says a place has closed, a road is closed, or a route took an illegal turn, the agent can send it to the [Feedback API](https://docs.mapbox.com/api/feedback/) (`POST /user-feedback/v1/feedback`, Public Preview) with the text, a category and the location, usually taken from an earlier search or directions result. It returns the report's id and status (`received`). diff --git a/docs/importing-tools.md b/docs/importing-tools.md index 8b982b9d..7e3c253e 100644 --- a/docs/importing-tools.md +++ b/docs/importing-tools.md @@ -332,15 +332,12 @@ import type { PromptInstance } from '@mapbox/mcp-server/prompts'; Here's how you might use these exports to build your own MCP server with a subset of tools. Each tool and resource registers itself with `installTo(server)`: ```typescript -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { McpServer } from '@modelcontextprotocol/server'; +import { StdioServerTransport } from '@modelcontextprotocol/server/stdio'; import { getAllTools } from '@mapbox/mcp-server/tools'; import { getAllResources } from '@mapbox/mcp-server/resources'; -import { - buildServerInstructions, - publishJsonSchema2020 -} from '@mapbox/mcp-server/utils'; +import { buildServerInstructions } from '@mapbox/mcp-server/utils'; // Pick only the tools you need. render_map_tool is only available through // the registry, and it needs the map UI resource from getAllResources(). @@ -363,11 +360,6 @@ const server = new McpServer( } ); -// Publish tool schemas as JSON Schema 2020-12 instead of the SDK's draft-07. -// Some clients (e.g. Claude Desktop) reject draft-07 output schemas. -// Must be called before the first tool is installed. -publishJsonSchema2020(server); - tools.forEach((tool) => tool.installTo(server)); getAllResources().forEach((resource) => resource.installTo(server)); @@ -378,9 +370,11 @@ await server.connect(transport); ### Server setup helpers -- `publishJsonSchema2020(server)`: converts every tool's `inputSchema`/`outputSchema` in `tools/list` from draft-07 to JSON Schema 2020-12. Call it on the `McpServer` before installing any tool. - `buildServerInstructions(toolNames)`: returns the server `instructions` text for the given enabled tool names, or `undefined` when `render_map_tool` isn't among them. -- `toJsonSchema2020(schema)`: the converter behind `publishJsonSchema2020`, for converting a single schema. +- `toJsonSchema2020(schema)`: converts a single draft-07 JSON Schema to JSON Schema 2020-12. +- `publishJsonSchema2020(server)`: deprecated no-op. MCP SDK v1 published tool schemas as draft-07 and this converted them; SDK v2 publishes 2020-12 itself, so the call can be removed. + +The tools and resources are built against MCP TypeScript SDK v2 (`@modelcontextprotocol/server`), so `installTo` needs a v2 `McpServer`. A v1 `McpServer` from `@modelcontextprotocol/sdk` won't work. ### Directions routes by reference diff --git a/evals/tool-selection/harness.ts b/evals/tool-selection/harness.ts index 1c6460d6..a206c4e5 100644 --- a/evals/tool-selection/harness.ts +++ b/evals/tool-selection/harness.ts @@ -19,9 +19,8 @@ import OpenAI from 'openai'; import { readFileSync } from 'node:fs'; import { dirname, resolve } from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import { Client } from '@modelcontextprotocol/sdk/client/index.js'; -import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js'; +import { Client } from '@modelcontextprotocol/client'; +import { McpServer, InMemoryTransport } from '@modelcontextprotocol/server'; const here = dirname(fileURLToPath(import.meta.url)); const SERVER_ROOT = resolve( diff --git a/examples/import-example.ts b/examples/import-example.ts index 357dbd13..7c19c1d5 100644 --- a/examples/import-example.ts +++ b/examples/import-example.ts @@ -8,8 +8,8 @@ * a custom MCP server with a subset of Mapbox tools. */ -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 pre-configured tool instances import { @@ -48,9 +48,9 @@ async function createCustomMcpServer() { // Advanced usage: Create tools with custom HTTP pipeline async function createServerWithCustomPipeline() { - const { McpServer } = await import('@modelcontextprotocol/sdk/server/mcp.js'); + const { McpServer } = await import('@modelcontextprotocol/server'); const { StdioServerTransport } = - await import('@modelcontextprotocol/sdk/server/stdio.js'); + await import('@modelcontextprotocol/server/stdio'); const { DirectionsTool } = await import('@mapbox/mcp-server/tools'); const { HttpPipeline, UserAgentPolicy, RetryPolicy } = await import('@mapbox/mcp-server/utils'); @@ -107,8 +107,7 @@ async function createServerWithCustomPipeline() { // Selective tool imports: Import only what you need to keep bundle size small async function selectiveImports() { - const { McpServer: Server } = - await import('@modelcontextprotocol/sdk/server/mcp.js'); + const { McpServer: Server } = await import('@modelcontextprotocol/server'); // Import just the Turf.js geometry tools (no HTTP calls needed) const { area, distance, bearing, centroid } = diff --git a/package-lock.json b/package-lock.json index 46e4392d..28a61d10 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,8 +10,9 @@ "hasInstallScript": true, "license": "MIT", "dependencies": { - "@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", @@ -31,6 +32,8 @@ "devDependencies": { "@anthropic-ai/sdk": "^0.133.0", "@eslint/js": "^9.27.0", + "@modelcontextprotocol/client": "^2.3.1", + "@modelcontextprotocol/node": "^2.1.1", "@types/node": "^22.0.0", "@typescript-eslint/eslint-plugin": "^8.58.2", "@typescript-eslint/parser": "^8.58.2", @@ -1748,9 +1751,10 @@ } }, "node_modules/@hono/node-server": { - "version": "1.19.14", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", - "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", + "version": "1.19.17", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.17.tgz", + "integrity": "sha512-dSneS5qhiauZWGDCeK4o695Xd9nUNjviSZCMQrj10eetr8Uln1ucn6bbphOM6UynAMMtNIzZNSpL9vnASJwrPQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=18.14.1" @@ -1913,24 +1917,62 @@ "url": "https://opencollective.com/js-sdsl" } }, + "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.5.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/ext-apps/-/ext-apps-1.5.0.tgz", - "integrity": "sha512-q4fut89TOoP2LEPHSGfZErIf1K1xOTTzV+41h/bB2BqKw2gKb0uLKbHusOy1UtbY0puS16zBho/vFp3f5XMVbQ==", + "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/*" ], + "dependencies": { + "@standard-schema/spec": "^1.1.0" + }, "engines": { "node": ">=20" }, "peerDependencies": { - "@modelcontextprotocol/sdk": "^1.29.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 }, @@ -1939,44 +1981,34 @@ } } }, - "node_modules/@modelcontextprotocol/sdk": { - "version": "1.30.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.30.0.tgz", - "integrity": "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA==", - "license": "MIT", + "node_modules/@modelcontextprotocol/node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/node/-/node-2.1.1.tgz", + "integrity": "sha512-EY92OAXN2xYL7tNS/MVsWJW1v8VHAPwl9B4Ca3pCrjmvqyC+S1C/6UHsMxF2GoFPraS5Yyg56vFz9brVpFojMA==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "@hono/node-server": "^1.19.9 || ^2.0.5", - "ajv": "^8.17.1", - "ajv-formats": "^3.0.1", - "content-type": "^1.0.5", - "cors": "^2.8.5", - "cross-spawn": "^7.0.5", - "eventsource": "^3.0.2", - "eventsource-parser": "^3.0.0", - "express": "^5.2.1", - "express-rate-limit": "^8.2.1", - "hono": "^4.11.4", - "jose": "^6.1.3", - "json-schema-typed": "^8.0.2", - "pkce-challenge": "^5.0.0", - "raw-body": "^3.0.0", - "zod": "^3.25 || ^4.0", - "zod-to-json-schema": "^3.25.1" + "@hono/node-server": "^1.19.9", + "hono": "^4.11.4" }, "engines": { - "node": ">=18" + "node": ">=20" }, "peerDependencies": { - "@cfworker/json-schema": "^4.1.1", - "zod": "^3.25 || ^4.0" + "@modelcontextprotocol/server": "^2.3.0" + } + }, + "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" }, - "peerDependenciesMeta": { - "@cfworker/json-schema": { - "optional": true - }, - "zod": { - "optional": false - } + "engines": { + "node": ">=20" } }, "node_modules/@napi-rs/wasm-runtime": { @@ -3704,7 +3736,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/@turf/along": { @@ -6453,19 +6484,6 @@ "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", "license": "BSD-2-Clause" }, - "node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "license": "MIT", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/acorn": { "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", @@ -6506,39 +6524,6 @@ "node": ">= 14" } }, - "node_modules/ajv": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", - "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", - "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", - "license": "MIT", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, "node_modules/ansi-escapes": { "version": "7.3.0", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", @@ -6688,30 +6673,6 @@ "readable-stream": "^3.4.0" } }, - "node_modules/body-parser": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", - "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", - "license": "MIT", - "dependencies": { - "bytes": "^3.1.2", - "content-type": "^1.0.5", - "debug": "^4.4.3", - "http-errors": "^2.0.0", - "iconv-lite": "^0.7.0", - "on-finished": "^2.4.1", - "qs": "^6.14.1", - "raw-body": "^3.0.1", - "type-is": "^2.0.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/braces": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", @@ -6783,15 +6744,6 @@ "ieee754": "^1.1.13" } }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/call-bind": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", @@ -7194,28 +7146,6 @@ "tinyqueue": "^2.0.3" } }, - "node_modules/content-disposition": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", - "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -7223,41 +7153,6 @@ "dev": true, "license": "MIT" }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "license": "MIT", - "engines": { - "node": ">=6.6.0" - } - }, - "node_modules/cors": { - "version": "2.8.6", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", - "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", - "license": "MIT", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -7541,15 +7436,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/detect-file": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", @@ -7597,12 +7483,6 @@ "integrity": "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==", "license": "ISC" }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT" - }, "node_modules/electron-to-chromium": { "version": "1.5.336", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.336.tgz", @@ -7616,15 +7496,6 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "license": "MIT" }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/enhanced-resolve": { "version": "5.20.1", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", @@ -7756,12 +7627,6 @@ "node": ">=6" } }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT" - }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -8227,15 +8092,6 @@ "node": ">=0.10.0" } }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/eventemitter3": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", @@ -8256,9 +8112,9 @@ } }, "node_modules/eventsource-parser": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", - "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "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": ">=18.0.0" @@ -8287,67 +8143,6 @@ "node": ">=12.0.0" } }, - "node_modules/express": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", - "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", - "license": "MIT", - "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.1", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "depd": "^2.0.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.7", - "qs": "^6.14.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express-rate-limit": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.2.tgz", - "integrity": "sha512-77VmFeJkO0/rvimEDuUC5H30oqUC4EyOhyGccfqoLebB0oiEYfM7nwPrsDsBL1gsTpwfzX8SFy2MT3TDyRq+bg==", - "license": "MIT", - "dependencies": { - "ip-address": "10.1.0" - }, - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://github.com/sponsors/express-rate-limit" - }, - "peerDependencies": { - "express": ">= 4.11" - } - }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -8398,22 +8193,6 @@ "dev": true, "license": "Unlicense" }, - "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" - }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -8480,27 +8259,6 @@ "node": ">=8" } }, - "node_modules/finalhandler": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", - "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -8626,30 +8384,12 @@ "node": ">=12.20.0" } }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/forwarded-parse": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/forwarded-parse/-/forwarded-parse-2.1.2.tgz", "integrity": "sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==", "license": "MIT" }, - "node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/fs-extra": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", @@ -9065,9 +8805,10 @@ } }, "node_modules/hono": { - "version": "4.12.12", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.12.tgz", - "integrity": "sha512-p1JfQMKaceuCbpJKAPKVqyqviZdS0eUxH9v82oWo1kb9xjQ5wA6iP3FNVAPDFlz5/p7d45lO+BpSk1tuSZMF4Q==", + "version": "4.13.13", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.13.tgz", + "integrity": "sha512-CQ46U0ZkAGmbT/4UxdzzGJpacP2IeKgY4a5/tOI9AABbpOMfK739wfDXmv1usCk+3RkKj1hQy4/fjhiwa2xlrA==", + "dev": true, "license": "MIT", "engines": { "node": ">=16.9.0" @@ -9080,26 +8821,6 @@ "dev": true, "license": "MIT" }, - "node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "license": "MIT", - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/https-proxy-agent": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", @@ -9133,6 +8854,7 @@ "version": "0.7.2", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "dev": true, "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -9256,6 +8978,7 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, "license": "ISC" }, "node_modules/ini": { @@ -9397,15 +9120,6 @@ "node": ">=10.13.0" } }, - "node_modules/ip-address": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", - "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, "node_modules/ipaddr.js": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.4.0.tgz", @@ -9528,12 +9242,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "license": "MIT" - }, "node_modules/is-relative": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", @@ -9754,18 +9462,6 @@ "node": ">=16" } }, - "node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" - }, - "node_modules/json-schema-typed": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", - "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", - "license": "BSD-2-Clause" - }, "node_modules/json-stable-stringify": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.3.0.tgz", @@ -10394,27 +10090,6 @@ "node": ">= 0.4" } }, - "node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/merge-descriptors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -10440,31 +10115,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", @@ -10614,15 +10264,6 @@ "dev": true, "license": "MIT" }, - "node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/neo-async": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", @@ -10697,27 +10338,6 @@ "dev": true, "license": "MIT" }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", @@ -10767,27 +10387,6 @@ ], "license": "MIT" }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, "node_modules/onetime": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", @@ -11083,15 +10682,6 @@ "node": ">=0.10.0" } }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/patch-package": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/patch-package/-/patch-package-8.0.1.tgz", @@ -11201,16 +10791,6 @@ "node": ">=0.10.0" } }, - "node_modules/path-to-regexp": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", - "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -11475,28 +11055,6 @@ "node": ">=12.0.0" } }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/proxy-addr/node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -11507,51 +11065,12 @@ "node": ">=6" } }, - "node_modules/qs": { - "version": "6.15.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", - "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/quickselect": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-2.0.0.tgz", "integrity": "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==", "license": "ISC" }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", - "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.7.0", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, "node_modules/rbush": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/rbush/-/rbush-3.0.1.tgz", @@ -11619,15 +11138,6 @@ "node": ">=0.10.0" } }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/require-in-the-middle": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-8.0.1.tgz", @@ -11888,22 +11398,6 @@ "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.15" } }, - "node_modules/router": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "depd": "^2.0.0", - "is-promise": "^4.0.0", - "parseurl": "^1.3.3", - "path-to-regexp": "^8.0.0" - }, - "engines": { - "node": ">= 18" - } - }, "node_modules/run-async": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz", @@ -11949,6 +11443,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, "license": "MIT" }, "node_modules/semver": { @@ -11963,51 +11458,6 @@ "node": ">=10" } }, - "node_modules/send": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", - "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.3", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.1", - "mime-types": "^3.0.2", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/serve-static": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", - "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", - "license": "MIT", - "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -12025,12 +11475,6 @@ "node": ">= 0.4" } }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "license": "ISC" - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -12052,78 +11496,6 @@ "node": ">=8" } }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", - "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/siginfo": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", @@ -12233,15 +11605,6 @@ "fast-sha256": "^1.3.0" } }, - "node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/std-env": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.0.0.tgz", @@ -12524,15 +11887,6 @@ "node": ">=8.0" } }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, "node_modules/topojson-client": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/topojson-client/-/topojson-client-3.1.0.tgz", @@ -12709,20 +12063,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/type-is": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", - "license": "MIT", - "dependencies": { - "content-type": "^1.0.5", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/typescript": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz", @@ -12800,15 +12140,6 @@ "node": ">= 10.0.0" } }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", @@ -12867,15 +12198,6 @@ "node": ">= 10.13.0" } }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/vite": { "version": "8.0.8", "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.8.tgz", @@ -13179,12 +12501,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" - }, "node_modules/xdg-basedir": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-5.1.0.tgz", @@ -13343,15 +12659,6 @@ "funding": { "url": "https://github.com/sponsors/colinhacks" } - }, - "node_modules/zod-to-json-schema": { - "version": "3.25.2", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", - "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", - "license": "ISC", - "peerDependencies": { - "zod": "^3.25.28 || ^4" - } } } } diff --git a/package.json b/package.json index 6b53b409..7d953a1d 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,8 @@ "devDependencies": { "@anthropic-ai/sdk": "^0.133.0", "@eslint/js": "^9.27.0", + "@modelcontextprotocol/client": "^2.3.1", + "@modelcontextprotocol/node": "^2.1.1", "@types/node": "^22.0.0", "@typescript-eslint/eslint-plugin": "^8.58.2", "@typescript-eslint/parser": "^8.58.2", @@ -85,8 +87,9 @@ "mcp" ], "dependencies": { - "@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", diff --git a/patches/@modelcontextprotocol+sdk+1.30.0.patch b/patches/@modelcontextprotocol+sdk+1.30.0.patch deleted file mode 100644 index 3471ba41..00000000 --- a/patches/@modelcontextprotocol+sdk+1.30.0.patch +++ /dev/null @@ -1,66 +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..2de8d57 100644 ---- a/node_modules/@modelcontextprotocol/sdk/dist/cjs/server/mcp.js -+++ b/node_modules/@modelcontextprotocol/sdk/dist/cjs/server/mcp.js -@@ -197,15 +197,20 @@ 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`); -+ // 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 = (0, zod_compat_js_1.normalizeObjectSchema)(tool.outputSchema); -- const parseResult = await (0, zod_compat_js_1.safeParseAsync)(outputObj, result.structuredContent); -- 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}`); -+ else { -+ // if the tool has an output schema, validate structured content -+ const outputObj = (0, zod_compat_js_1.normalizeObjectSchema)(tool.outputSchema); -+ const parseResult = await (0, zod_compat_js_1.safeParseAsync)(outputObj, result.structuredContent); -+ if (!parseResult.success) { -+ const error = 'error' in parseResult ? parseResult.error : 'Unknown error'; -+ const errorMessage = (0, zod_compat_js_1.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/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 1a6f86d4..ce620a5e 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/scripts/dev-http-server.ts b/scripts/dev-http-server.ts index eb092202..a258562d 100644 --- a/scripts/dev-http-server.ts +++ b/scripts/dev-http-server.ts @@ -25,8 +25,8 @@ */ import { createServer } from 'node:http'; -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; +import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node'; +import { McpServer } from '@modelcontextprotocol/server'; import { registerAppResource, RESOURCE_MIME_TYPE @@ -112,7 +112,7 @@ const httpServer = createServer((req, res) => { // manual testing, no session/resumability concerns to worry about. void (async () => { const server = buildServer(); - const transport = new StreamableHTTPServerTransport({ + const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined }); try { diff --git a/src/index.ts b/src/index.ts index 5afb5e22..46c96a66 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,18 +8,12 @@ import { readFileSync } from 'node:fs'; 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 { registerAppResource, RESOURCE_MIME_TYPE } from '@modelcontextprotocol/ext-apps/server'; -import { - ListPromptsRequestSchema, - GetPromptRequestSchema, - CompleteRequestSchema -} from '@modelcontextprotocol/sdk/types.js'; import { parseToolConfigFromArgs, filterTools } from './config/toolConfig.js'; import { getCoreTools, @@ -30,7 +24,6 @@ import { getAllResources } from './resources/resourceRegistry.js'; import { getAllPrompts, getPromptByName } from './prompts/promptRegistry.js'; import { completePromptArgument } from './completions/index.js'; import { getVersionInfo } from './utils/versionUtils.js'; -import { publishJsonSchema2020 } from './utils/jsonSchema2020.js'; import { buildServerInstructions } from './utils/serverInstructions.js'; import { handleCliMetadataArgs } from './cli.js'; import { @@ -136,9 +129,6 @@ const server = new McpServer( } ); -// Must run before the first tool is registered — see jsonSchema2020.ts. -publishJsonSchema2020(server); - // Register only core tools before connection // Capability-dependent tools will be registered dynamically after connection enabledCoreTools.forEach((tool) => { @@ -152,6 +142,10 @@ const regularResources = allResources.filter((r) => !r.uri.startsWith('ui://')); // Register MCP Apps UI resources using registerAppResource // IMPORTANT: Use RESOURCE_MIME_TYPE which is "text/html;profile=mcp-app" // This tells clients (like Claude Desktop) that this is an MCP App +// +// The casts are for the CommonJS build: there, ext-apps' declarations resolve +// McpServer from the SDK's ESM types and ours from its CJS types, so the +// same class doesn't type-check against itself. uiResources.forEach((resource) => { registerAppResource( // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -173,7 +167,7 @@ regularResources.forEach((resource) => { }); // Register prompt handlers -server.server.setRequestHandler(ListPromptsRequestSchema, async () => { +server.server.setRequestHandler('prompts/list', async () => { const allPrompts = getAllPrompts(); return { prompts: allPrompts.map((prompt) => prompt.getMetadata()) @@ -185,7 +179,7 @@ server.server.setRequestHandler(ListPromptsRequestSchema, async () => { // TODO: Remove this workaround when SDK fixes their type definitions // eslint-disable-next-line @typescript-eslint/no-explicit-any (server.server as any).setRequestHandler( - GetPromptRequestSchema, + 'prompts/get', async (request: any) => { const { name, arguments: args } = request.params; @@ -213,7 +207,7 @@ server.server.setRequestHandler(ListPromptsRequestSchema, async () => { // Register completion handler // eslint-disable-next-line @typescript-eslint/no-explicit-any (server.server as any).setRequestHandler( - CompleteRequestSchema, + 'completion/complete', // eslint-disable-next-line @typescript-eslint/no-explicit-any async (request: any) => { const { ref, argument } = request.params; diff --git a/src/prompts/BasePrompt.ts b/src/prompts/BasePrompt.ts index 70f1f284..71d13df1 100644 --- a/src/prompts/BasePrompt.ts +++ b/src/prompts/BasePrompt.ts @@ -1,11 +1,11 @@ // Copyright (c) Mapbox, Inc. // Licensed under the MIT License. -import { - type Prompt, - type PromptArgument, - type PromptMessage -} from '@modelcontextprotocol/sdk/types.js'; +import type { + Prompt, + PromptArgument, + PromptMessage +} from '@modelcontextprotocol/server'; /** * Base class for all MCP prompts. diff --git a/src/prompts/FindPlacesNearbyPrompt.ts b/src/prompts/FindPlacesNearbyPrompt.ts index cb9eb0fa..91fa774c 100644 --- a/src/prompts/FindPlacesNearbyPrompt.ts +++ b/src/prompts/FindPlacesNearbyPrompt.ts @@ -5,7 +5,7 @@ import { BasePrompt } from './BasePrompt.js'; import type { PromptArgument, PromptMessage -} from '@modelcontextprotocol/sdk/types.js'; +} from '@modelcontextprotocol/server'; /** * Prompt for finding places near a location with optional map visualization. diff --git a/src/prompts/GetDirectionsPrompt.ts b/src/prompts/GetDirectionsPrompt.ts index 888d19ff..51870d2b 100644 --- a/src/prompts/GetDirectionsPrompt.ts +++ b/src/prompts/GetDirectionsPrompt.ts @@ -5,7 +5,7 @@ import { BasePrompt } from './BasePrompt.js'; import type { PromptArgument, PromptMessage -} from '@modelcontextprotocol/sdk/types.js'; +} from '@modelcontextprotocol/server'; /** * Prompt for getting turn-by-turn directions between two locations. diff --git a/src/prompts/SearchAlongRoutePrompt.ts b/src/prompts/SearchAlongRoutePrompt.ts index b9364c35..d44958d1 100644 --- a/src/prompts/SearchAlongRoutePrompt.ts +++ b/src/prompts/SearchAlongRoutePrompt.ts @@ -5,7 +5,7 @@ import { BasePrompt } from './BasePrompt.js'; import type { PromptArgument, PromptMessage -} from '@modelcontextprotocol/sdk/types.js'; +} from '@modelcontextprotocol/server'; /** * Prompt for finding places along a route between two locations. diff --git a/src/prompts/ShowReachableAreasPrompt.ts b/src/prompts/ShowReachableAreasPrompt.ts index 0891c1ea..b3d9a2f8 100644 --- a/src/prompts/ShowReachableAreasPrompt.ts +++ b/src/prompts/ShowReachableAreasPrompt.ts @@ -5,7 +5,7 @@ import { BasePrompt } from './BasePrompt.js'; import type { PromptArgument, PromptMessage -} from '@modelcontextprotocol/sdk/types.js'; +} from '@modelcontextprotocol/server'; /** * Prompt for visualizing areas reachable within a specified time from a location. diff --git a/src/resources/BaseResource.ts b/src/resources/BaseResource.ts index 6c80beb1..e149ed93 100644 --- a/src/resources/BaseResource.ts +++ b/src/resources/BaseResource.ts @@ -2,16 +2,12 @@ // Licensed under the MIT License. import { AsyncLocalStorage } from 'node:async_hooks'; -import { - type McpServer, - ResourceTemplate -} from '@modelcontextprotocol/sdk/server/mcp.js'; -import type { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol.js'; +import { ResourceTemplate } from '@modelcontextprotocol/server'; import type { - ServerRequest, - ServerNotification, - ReadResourceResult -} from '@modelcontextprotocol/sdk/types.js'; + McpServer, + ReadResourceResult, + ServerContext +} from '@modelcontextprotocol/server'; /** * Base class for all MCP resources @@ -69,10 +65,10 @@ export abstract class BaseResource { ( uri: URL, _variables: Record, - extra: RequestHandlerExtra + ctx: ServerContext ) => this.invocationServer.run(server, () => - this.read(uri.toString(), extra) + this.read(uri.toString(), ctx) ) ); } else { @@ -80,12 +76,9 @@ export abstract class BaseResource { this.name, this.uri, metadata, - ( - uri: URL, - extra: RequestHandlerExtra - ) => + (uri: URL, ctx: ServerContext) => this.invocationServer.run(server, () => - this.read(uri.toString(), extra) + this.read(uri.toString(), ctx) ) ); } @@ -98,7 +91,7 @@ export abstract class BaseResource { */ abstract read( uri: string, - extra?: RequestHandlerExtra + extra?: ServerContext ): Promise; /** diff --git a/src/resources/MapboxApiBasedResource.ts b/src/resources/MapboxApiBasedResource.ts index 23801aa2..99a7e8d9 100644 --- a/src/resources/MapboxApiBasedResource.ts +++ b/src/resources/MapboxApiBasedResource.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 { - ServerRequest, - ServerNotification, - ReadResourceResult -} from '@modelcontextprotocol/sdk/types.js'; + ReadResourceResult, + ServerContext +} from '@modelcontextprotocol/server'; import { BaseResource } from './BaseResource.js'; import type { HttpRequest } from '../utils/types.js'; import { context, trace, SpanStatusCode } from '@opentelemetry/api'; @@ -54,12 +52,9 @@ export abstract class MapboxApiBasedResource extends BaseResource { /** * Validates and reads the resource. */ - async read( - uri: string, - extra?: RequestHandlerExtra - ): Promise { + async read(uri: string, extra?: ServerContext): Promise { // First check if token is provided via authentication context - const authToken = extra?.authInfo?.token; + const authToken = extra?.http?.authInfo?.token; const accessToken = authToken || MapboxApiBasedResource.mapboxAccessToken; if (!accessToken) { diff --git a/src/resources/category-list/CategoryListResource.ts b/src/resources/category-list/CategoryListResource.ts index 98fe3d64..0c901a9f 100644 --- a/src/resources/category-list/CategoryListResource.ts +++ b/src/resources/category-list/CategoryListResource.ts @@ -3,7 +3,7 @@ import { MapboxApiBasedResource } from '../MapboxApiBasedResource.js'; import type { HttpRequest } from '../../utils/types.js'; -import type { ReadResourceResult } from '@modelcontextprotocol/sdk/types.js'; +import type { ReadResourceResult } from '@modelcontextprotocol/server'; // Interface for the full API response from Mapbox interface MapboxApiResponse { diff --git a/src/resources/compute/ComputeResource.ts b/src/resources/compute/ComputeResource.ts index 7bca720f..ec49d81a 100644 --- a/src/resources/compute/ComputeResource.ts +++ b/src/resources/compute/ComputeResource.ts @@ -2,12 +2,10 @@ // Licensed under the MIT License. import { BaseResource } from '../BaseResource.js'; -import type { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol.js'; import type { ReadResourceResult, - ServerNotification, - ServerRequest -} from '@modelcontextprotocol/sdk/types.js'; + ServerContext +} from '@modelcontextprotocol/server'; import { resolveComputeRef } from '../../utils/computeRef.js'; /** @@ -29,7 +27,7 @@ export class ComputeResource extends BaseResource { async read( uri: string, // eslint-disable-next-line @typescript-eslint/no-unused-vars - _extra?: RequestHandlerExtra + _extra?: ServerContext ): Promise { const payload = resolveComputeRef(uri); diff --git a/src/resources/inline-payload/InlinePayloadResource.ts b/src/resources/inline-payload/InlinePayloadResource.ts index f8b626d9..d4e3b744 100644 --- a/src/resources/inline-payload/InlinePayloadResource.ts +++ b/src/resources/inline-payload/InlinePayloadResource.ts @@ -2,12 +2,10 @@ // Licensed under the MIT License. import { BaseResource } from '../BaseResource.js'; -import type { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol.js'; import type { ReadResourceResult, - ServerNotification, - ServerRequest -} from '@modelcontextprotocol/sdk/types.js'; + ServerContext +} from '@modelcontextprotocol/server'; import { resolveInlinePayloadRef } from '../../utils/inlinePayloadRef.js'; /** @@ -26,7 +24,7 @@ export class InlinePayloadResource extends BaseResource { async read( uri: string, // eslint-disable-next-line @typescript-eslint/no-unused-vars - _extra?: RequestHandlerExtra + _extra?: ServerContext ): Promise { const payload = resolveInlinePayloadRef(uri); diff --git a/src/resources/place-photo/PlacePhotoResource.ts b/src/resources/place-photo/PlacePhotoResource.ts index 302152f1..1cd45ace 100644 --- a/src/resources/place-photo/PlacePhotoResource.ts +++ b/src/resources/place-photo/PlacePhotoResource.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. import { lookup } from 'node:dns/promises'; -import type { ReadResourceResult } from '@modelcontextprotocol/sdk/types.js'; +import type { ReadResourceResult } from '@modelcontextprotocol/server'; import { MapboxApiBasedResource } from '../MapboxApiBasedResource.js'; import type { HttpRequest } from '../../utils/types.js'; import { diff --git a/src/resources/temporary/TemporaryDataResource.ts b/src/resources/temporary/TemporaryDataResource.ts index cb2a3e4f..a5613b25 100644 --- a/src/resources/temporary/TemporaryDataResource.ts +++ b/src/resources/temporary/TemporaryDataResource.ts @@ -2,12 +2,10 @@ // Licensed under the MIT License. import { BaseResource } from '../BaseResource.js'; -import type { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol.js'; import type { ReadResourceResult, - ServerNotification, - ServerRequest -} from '@modelcontextprotocol/sdk/types.js'; + ServerContext +} from '@modelcontextprotocol/server'; import { temporaryResourceManager } from '../../utils/temporaryResourceManager.js'; import { getOwnerKeyFromToken, ownerKeyMatches } from '../../utils/jwtUtils.js'; @@ -22,10 +20,7 @@ export class TemporaryDataResource extends BaseResource { 'Temporary storage for large tool responses. Data expires after TTL.'; readonly mimeType = 'application/json'; - async read( - uri: string, - extra?: RequestHandlerExtra - ): Promise { + async read(uri: string, extra?: ServerContext): Promise { const notFound: ReadResourceResult = { contents: [ { @@ -68,7 +63,7 @@ export class TemporaryDataResource extends BaseResource { // shared MAPBOX_ACCESS_TOKEN in a multi-tenant deployment or all env-less // reads would resolve to that one token. const requesterToken = - (extra?.authInfo?.token as string | undefined) ?? + (extra?.http?.authInfo?.token as string | undefined) ?? process.env.MAPBOX_ACCESS_TOKEN; if ( !ownerKeyMatches(resource.owner, getOwnerKeyFromToken(requesterToken)) diff --git a/src/resources/tool-response/ToolResponseResource.ts b/src/resources/tool-response/ToolResponseResource.ts index c9b7bb4d..d95fd043 100644 --- a/src/resources/tool-response/ToolResponseResource.ts +++ b/src/resources/tool-response/ToolResponseResource.ts @@ -1,7 +1,7 @@ // Copyright (c) Mapbox, Inc. // Licensed under the MIT License. -import type { ReadResourceResult } from '@modelcontextprotocol/sdk/types.js'; +import type { ReadResourceResult } from '@modelcontextprotocol/server'; import { MapboxApiBasedResource } from '../MapboxApiBasedResource.js'; import type { HttpRequest } from '../../utils/types.js'; import { resolveResponseRef } from '../../utils/responseRef.js'; diff --git a/src/resources/ui-apps/MapAppUIResource.ts b/src/resources/ui-apps/MapAppUIResource.ts index 152f989f..cc20d8fb 100644 --- a/src/resources/ui-apps/MapAppUIResource.ts +++ b/src/resources/ui-apps/MapAppUIResource.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 type { HttpRequest } from '../../utils/types.js'; @@ -44,12 +42,9 @@ export class MapAppUIResource extends BaseResource { (() => process.env.MAPBOX_API_ENDPOINT || 'https://api.mapbox.com/'); } - async read( - _uri: string, - extra?: RequestHandlerExtra - ): Promise { + async read(_uri: string, extra?: ServerContext): Promise { const accessToken = - (extra?.authInfo?.token as string | undefined) || + (extra?.http?.authInfo?.token as string | undefined) || process.env.MAPBOX_ACCESS_TOKEN || ''; diff --git a/src/resources/version/VersionResource.ts b/src/resources/version/VersionResource.ts index d7645dec..2aabb7ce 100644 --- a/src/resources/version/VersionResource.ts +++ b/src/resources/version/VersionResource.ts @@ -3,7 +3,7 @@ import { BaseResource } from '../BaseResource.js'; import { getVersionInfo } from '../../utils/versionUtils.js'; -import type { ReadResourceResult } from '@modelcontextprotocol/sdk/types.js'; +import type { ReadResourceResult } from '@modelcontextprotocol/server'; /** * Resource exposing MCP server version information. diff --git a/src/tools/BaseTool.ts b/src/tools/BaseTool.ts index eb771efd..d8dc0475 100644 --- a/src/tools/BaseTool.ts +++ b/src/tools/BaseTool.ts @@ -3,14 +3,13 @@ import type { McpServer, - RegisteredTool -} from '@modelcontextprotocol/sdk/server/mcp.js'; -import type { + RegisteredTool, ToolAnnotations, - CallToolResult -} from '@modelcontextprotocol/sdk/types.js'; -import type { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol.js'; + CallToolResult, + ServerContext +} from '@modelcontextprotocol/server'; import { AsyncLocalStorage } from 'node:async_hooks'; +import { lenientOutputSchema } from './lenientOutputSchema.js'; import type { ZodTypeAny } from 'zod'; import type { z } from 'zod'; @@ -101,10 +100,10 @@ export abstract class BaseTool< }; // Add outputSchema if provided — pass the full Zod schema (not just .shape) - // so that .passthrough() and other schema-level settings are preserved when - // the MCP SDK converts it for structured-content validation. + // so that .passthrough() and other schema-level settings are preserved in + // tools/list. Validation only warns; see lenientOutputSchema.ts. if (this.outputSchema) { - config.outputSchema = this.outputSchema; + config.outputSchema = lenientOutputSchema(this.outputSchema, this.name); } // Add _meta for MCP Apps support if provided (includes CSP configuration) @@ -116,8 +115,27 @@ export abstract class BaseTool< this.name, config, // eslint-disable-next-line @typescript-eslint/no-explicit-any - (args: any, extra: any) => - this.invocationServer.run(server, () => this.run(args, extra)) + async (args: any, ctx: ServerContext) => { + const result = await this.invocationServer.run(server, () => + 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. Some + // tools fall back to text only (validateAndSanitizeOutput drops + // structuredContent that doesn't match), so 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; + } ); } @@ -126,8 +144,7 @@ export abstract class BaseTool< */ abstract run( rawInput: unknown, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - extra?: RequestHandlerExtra + extra?: ServerContext ): Promise; /** diff --git a/src/tools/MapboxApiBasedTool.ts b/src/tools/MapboxApiBasedTool.ts index 4febfcc0..cfe8f769 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 } from 'zod'; -import { BaseTool } from './BaseTool.js'; import type { CallToolResult, - ToolAnnotations -} from '@modelcontextprotocol/sdk/types.js'; + ToolAnnotations, + ServerContext +} from '@modelcontextprotocol/server'; +import type { ZodTypeAny } 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'; @@ -80,14 +80,14 @@ export abstract class MapboxApiBasedTool< */ async run( rawInput: unknown, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - extra?: RequestHandlerExtra + + 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/area-tool/AreaTool.ts b/src/tools/area-tool/AreaTool.ts index 1f57fb73..9306659a 100644 --- a/src/tools/area-tool/AreaTool.ts +++ b/src/tools/area-tool/AreaTool.ts @@ -7,7 +7,7 @@ import { createLocalToolExecutionContext } from '../../utils/tracing.js'; import { BaseTool } from '../BaseTool.js'; import { AreaInputSchema } from './AreaTool.input.schema.js'; import { AreaOutputSchema, type AreaOutput } from './AreaTool.output.schema.js'; -import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; +import type { CallToolResult } from '@modelcontextprotocol/server'; /** * AreaTool - Calculate the area of a polygon or multipolygon diff --git a/src/tools/bearing-tool/BearingTool.ts b/src/tools/bearing-tool/BearingTool.ts index 10269b58..0acc7fd8 100644 --- a/src/tools/bearing-tool/BearingTool.ts +++ b/src/tools/bearing-tool/BearingTool.ts @@ -10,7 +10,7 @@ import { BearingOutputSchema, type BearingOutput } from './BearingTool.output.schema.js'; -import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; +import type { CallToolResult } from '@modelcontextprotocol/server'; /** * BearingTool - Calculate the bearing between two geographic coordinates diff --git a/src/tools/bounding-box-tool/BoundingBoxTool.ts b/src/tools/bounding-box-tool/BoundingBoxTool.ts index 601c4452..addabe74 100644 --- a/src/tools/bounding-box-tool/BoundingBoxTool.ts +++ b/src/tools/bounding-box-tool/BoundingBoxTool.ts @@ -16,7 +16,7 @@ import { BoundingBoxOutputSchema, type BoundingBoxOutput } from './BoundingBoxTool.output.schema.js'; -import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; +import type { CallToolResult } from '@modelcontextprotocol/server'; /** * BoundingBoxTool - Calculate the bounding box of a geometry diff --git a/src/tools/buffer-tool/BufferTool.ts b/src/tools/buffer-tool/BufferTool.ts index a566c785..5c7f34e4 100644 --- a/src/tools/buffer-tool/BufferTool.ts +++ b/src/tools/buffer-tool/BufferTool.ts @@ -10,7 +10,7 @@ import { BufferOutputSchema, type BufferOutput } from './BufferTool.output.schema.js'; -import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; +import type { CallToolResult } from '@modelcontextprotocol/server'; /** * BufferTool - Create a buffer zone around a geometry diff --git a/src/tools/category-list-tool/CategoryListTool.ts b/src/tools/category-list-tool/CategoryListTool.ts index d97c4283..9370b0b6 100644 --- a/src/tools/category-list-tool/CategoryListTool.ts +++ b/src/tools/category-list-tool/CategoryListTool.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. 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 type { CategoryListInput } from './CategoryListTool.input.schema.js'; import { CategoryListInputSchema } from './CategoryListTool.input.schema.js'; diff --git a/src/tools/category-search-tool/CategorySearchTool.ts b/src/tools/category-search-tool/CategorySearchTool.ts index 51728465..80978652 100644 --- a/src/tools/category-search-tool/CategorySearchTool.ts +++ b/src/tools/category-search-tool/CategorySearchTool.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 { CategorySearchInputSchema } from './CategorySearchTool.input.schema.js'; import { CategorySearchResponseSchema } from './CategorySearchTool.output.schema.js'; diff --git a/src/tools/centroid-tool/CentroidTool.ts b/src/tools/centroid-tool/CentroidTool.ts index 72b9cdb3..8893d707 100644 --- a/src/tools/centroid-tool/CentroidTool.ts +++ b/src/tools/centroid-tool/CentroidTool.ts @@ -10,7 +10,7 @@ import { CentroidOutputSchema, type CentroidOutput } from './CentroidTool.output.schema.js'; -import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; +import type { CallToolResult } from '@modelcontextprotocol/server'; /** * CentroidTool - Calculate the center point (centroid) of a polygon or multipolygon diff --git a/src/tools/convex-tool/ConvexTool.ts b/src/tools/convex-tool/ConvexTool.ts index 1fea5009..104f8468 100644 --- a/src/tools/convex-tool/ConvexTool.ts +++ b/src/tools/convex-tool/ConvexTool.ts @@ -10,7 +10,7 @@ import { ConvexOutputSchema, type ConvexOutput } from './ConvexTool.output.schema.js'; -import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; +import type { CallToolResult } from '@modelcontextprotocol/server'; export class ConvexTool extends BaseTool< typeof ConvexInputSchema, diff --git a/src/tools/destination-tool/DestinationTool.ts b/src/tools/destination-tool/DestinationTool.ts index ae8f4a98..6520429e 100644 --- a/src/tools/destination-tool/DestinationTool.ts +++ b/src/tools/destination-tool/DestinationTool.ts @@ -10,7 +10,7 @@ import { DestinationOutputSchema, type DestinationOutput } from './DestinationTool.output.schema.js'; -import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; +import type { CallToolResult } from '@modelcontextprotocol/server'; export class DestinationTool extends BaseTool< typeof DestinationInputSchema, diff --git a/src/tools/difference-tool/DifferenceTool.ts b/src/tools/difference-tool/DifferenceTool.ts index 6304f773..de463fcb 100644 --- a/src/tools/difference-tool/DifferenceTool.ts +++ b/src/tools/difference-tool/DifferenceTool.ts @@ -10,7 +10,7 @@ import { DifferenceOutputSchema, type DifferenceOutput } from './DifferenceTool.output.schema.js'; -import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; +import type { CallToolResult } from '@modelcontextprotocol/server'; import { renderHint } from '../../utils/storeMapPayload.js'; import { buildComputeRef } from '../../utils/computeRef.js'; diff --git a/src/tools/directions-tool/DirectionsTool.ts b/src/tools/directions-tool/DirectionsTool.ts index 45cb59d8..e0b40bd1 100644 --- a/src/tools/directions-tool/DirectionsTool.ts +++ b/src/tools/directions-tool/DirectionsTool.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 { cleanResponseData } from './cleanResponseData.js'; import { buildDirectionsRequestUrl } from './buildDirectionsRequestUrl.js'; import { DirectionsInputSchema } from './DirectionsTool.input.schema.js'; diff --git a/src/tools/distance-tool/DistanceTool.ts b/src/tools/distance-tool/DistanceTool.ts index 10ec8b4c..44b3a486 100644 --- a/src/tools/distance-tool/DistanceTool.ts +++ b/src/tools/distance-tool/DistanceTool.ts @@ -10,7 +10,7 @@ import { DistanceOutputSchema, type DistanceOutput } from './DistanceTool.output.schema.js'; -import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; +import type { CallToolResult } from '@modelcontextprotocol/server'; /** * DistanceTool - Calculate distance between two geographic coordinates diff --git a/src/tools/feedback-tool/FeedbackTool.ts b/src/tools/feedback-tool/FeedbackTool.ts index 97ad13d1..ebb72199 100644 --- a/src/tools/feedback-tool/FeedbackTool.ts +++ b/src/tools/feedback-tool/FeedbackTool.ts @@ -3,7 +3,7 @@ import { randomUUID } from 'node:crypto'; import { distance as turfDistance } from '@turf/turf'; -import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; +import type { CallToolResult } from '@modelcontextprotocol/server'; import { MapboxApiBasedTool } from '../MapboxApiBasedTool.js'; import { FeedbackInputSchema, diff --git a/src/tools/ground-location-tool/GroundLocationTool.ts b/src/tools/ground-location-tool/GroundLocationTool.ts index 29aa4b0c..5d5c7ca4 100644 --- a/src/tools/ground-location-tool/GroundLocationTool.ts +++ b/src/tools/ground-location-tool/GroundLocationTool.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 { GroundLocationInputSchema } from './GroundLocationTool.input.schema.js'; import { diff --git a/src/tools/intersect-tool/IntersectTool.ts b/src/tools/intersect-tool/IntersectTool.ts index 811b2b30..323a43f8 100644 --- a/src/tools/intersect-tool/IntersectTool.ts +++ b/src/tools/intersect-tool/IntersectTool.ts @@ -10,7 +10,7 @@ import { IntersectOutputSchema, type IntersectOutput } from './IntersectTool.output.schema.js'; -import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; +import type { CallToolResult } from '@modelcontextprotocol/server'; import { renderHint } from '../../utils/storeMapPayload.js'; import { buildComputeRef } from '../../utils/computeRef.js'; diff --git a/src/tools/isochrone-tool/IsochroneTool.ts b/src/tools/isochrone-tool/IsochroneTool.ts index e30e894d..d93bc935 100644 --- a/src/tools/isochrone-tool/IsochroneTool.ts +++ b/src/tools/isochrone-tool/IsochroneTool.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 { IsochroneInputSchema } from './IsochroneTool.input.schema.js'; import { buildIsochroneRequestUrl } from './buildIsochroneRequestUrl.js'; diff --git a/src/tools/length-tool/LengthTool.ts b/src/tools/length-tool/LengthTool.ts index b2480182..0ac081b3 100644 --- a/src/tools/length-tool/LengthTool.ts +++ b/src/tools/length-tool/LengthTool.ts @@ -10,7 +10,7 @@ import { LengthOutputSchema, type LengthOutput } from './LengthTool.output.schema.js'; -import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; +import type { CallToolResult } from '@modelcontextprotocol/server'; export class LengthTool extends BaseTool< typeof LengthInputSchema, diff --git a/src/tools/lenientOutputSchema.ts b/src/tools/lenientOutputSchema.ts new file mode 100644 index 00000000..c81b8bbd --- /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/map-matching-tool/MapMatchingTool.ts b/src/tools/map-matching-tool/MapMatchingTool.ts index 656a71d3..089180f8 100644 --- a/src/tools/map-matching-tool/MapMatchingTool.ts +++ b/src/tools/map-matching-tool/MapMatchingTool.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 { MapMatchingInputSchema } from './MapMatchingTool.input.schema.js'; import { buildMapMatchingRequestUrl } from './buildMapMatchingRequestUrl.js'; import { diff --git a/src/tools/matrix-tool/MatrixTool.ts b/src/tools/matrix-tool/MatrixTool.ts index ba993fdc..56d65400 100644 --- a/src/tools/matrix-tool/MatrixTool.ts +++ b/src/tools/matrix-tool/MatrixTool.ts @@ -4,7 +4,7 @@ import type { z } from 'zod'; import { URLSearchParams } from 'node:url'; 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 { MatrixInputSchema } from './MatrixTool.input.schema.js'; import { diff --git a/src/tools/midpoint-tool/MidpointTool.ts b/src/tools/midpoint-tool/MidpointTool.ts index d29f5803..c48df664 100644 --- a/src/tools/midpoint-tool/MidpointTool.ts +++ b/src/tools/midpoint-tool/MidpointTool.ts @@ -10,7 +10,7 @@ import { MidpointOutputSchema, type MidpointOutput } from './MidpointTool.output.schema.js'; -import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; +import type { CallToolResult } from '@modelcontextprotocol/server'; /** * MidpointTool - Calculate the geographic midpoint between two coordinates diff --git a/src/tools/nearest-point-on-line-tool/NearestPointOnLineTool.ts b/src/tools/nearest-point-on-line-tool/NearestPointOnLineTool.ts index 4ce44135..82e750c1 100644 --- a/src/tools/nearest-point-on-line-tool/NearestPointOnLineTool.ts +++ b/src/tools/nearest-point-on-line-tool/NearestPointOnLineTool.ts @@ -10,7 +10,7 @@ import { NearestPointOnLineOutputSchema, type NearestPointOnLineOutput } from './NearestPointOnLineTool.output.schema.js'; -import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; +import type { CallToolResult } from '@modelcontextprotocol/server'; export class NearestPointOnLineTool extends BaseTool< typeof NearestPointOnLineInputSchema, diff --git a/src/tools/nearest-point-tool/NearestPointTool.ts b/src/tools/nearest-point-tool/NearestPointTool.ts index b5aa725a..2c73310f 100644 --- a/src/tools/nearest-point-tool/NearestPointTool.ts +++ b/src/tools/nearest-point-tool/NearestPointTool.ts @@ -10,7 +10,7 @@ import { NearestPointOutputSchema, type NearestPointOutput } from './NearestPointTool.output.schema.js'; -import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; +import type { CallToolResult } from '@modelcontextprotocol/server'; export class NearestPointTool extends BaseTool< typeof NearestPointInputSchema, diff --git a/src/tools/optimization-tool/OptimizationTool.ts b/src/tools/optimization-tool/OptimizationTool.ts index 6c799b25..e717c12f 100644 --- a/src/tools/optimization-tool/OptimizationTool.ts +++ b/src/tools/optimization-tool/OptimizationTool.ts @@ -11,7 +11,7 @@ import { OptimizationOutputSchema, type OptimizationOutput } from './OptimizationTool.output.schema.js'; -import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; +import type { CallToolResult } from '@modelcontextprotocol/server'; import type { ToolExecutionContext } from '../../utils/tracing.js'; import type { HttpRequest } from '../../utils/types.js'; import { buildOptimizationRequestUrl } from './buildOptimizationRequestUrl.js'; diff --git a/src/tools/optimization-tool/OptimizationV2Tool.input.schema.ts b/src/tools/optimization-tool/OptimizationV2Tool.input.schema.ts deleted file mode 100644 index bae11412..00000000 --- a/src/tools/optimization-tool/OptimizationV2Tool.input.schema.ts +++ /dev/null @@ -1,215 +0,0 @@ -// Copyright (c) Mapbox, Inc. -// Licensed under the MIT License. - -import z from 'zod'; -import { coordinateSchema } from '../../schemas/shared.js'; - -// Profile schema (driving, walking, cycling, driving-traffic) -const profileSchema = z - .enum([ - 'mapbox/driving', - 'mapbox/walking', - 'mapbox/cycling', - 'mapbox/driving-traffic' - ]) - .describe('Routing profile'); - -/** - * Input schema for OptimizationV2Tool (V2 API - Beta) - * - * The V2 API is currently in beta and requires early access. - * Supports advanced features: time windows, capacity limits, vehicle capabilities, - * shipments, breaks, and more. - */ - -const optionsSchema = z.object({ - objectives: z - .array( - z.enum(['min-total-travel-duration', 'min-schedule-completion-time']) - ) - .optional() - .describe('Optimization objectives') -}); - -const iso8601Schema = z - .string() - .regex( - /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?([+-]\d{2}:\d{2}|Z)?$/, - 'Must be a valid ISO 8601 datetime string (e.g., 2023-07-22T14:29:23-07:00)' - ); - -const timeWindowSchema = z.object({ - earliest: iso8601Schema.describe('Earliest time for this window'), - latest: iso8601Schema.describe('Latest time for this window'), - type: z - .enum(['strict', 'soft', 'soft_start', 'soft_end']) - .optional() - .describe('Type of time window constraint') -}); - -const vehicleBreakSchema = z.object({ - earliest_start: iso8601Schema.describe('Earliest start time for break'), - latest_end: iso8601Schema.describe('Latest end time for break'), - duration: z.number().int().positive().describe('Break duration in seconds') -}); - -const vehicleSchema = z.object({ - name: z.string().describe('Unique identifier for this vehicle'), - routing_profile: profileSchema - .optional() - .describe('Routing profile for this vehicle'), - start_location: z - .string() - .optional() - .describe( - 'Starting location as a STRING name (e.g., "location-0" for coordinates[0], NOT the integer 0)' - ), - end_location: z - .string() - .optional() - .describe( - 'Ending location as a STRING name (e.g., "location-0" for coordinates[0], NOT the integer 0)' - ), - capacities: z - .record(z.string(), z.number()) - .optional() - .describe('Custom capacity dimensions (e.g., weight, volume)'), - capabilities: z - .array(z.string()) - .optional() - .describe('Required vehicle capabilities'), - earliest_start: iso8601Schema - .optional() - .describe('Earliest operation start time'), - latest_end: iso8601Schema.optional().describe('Latest operation end time'), - breaks: z - .array(vehicleBreakSchema) - .optional() - .describe('Vehicle break requirements'), - loading_policy: z - .enum(['any', 'fifo', 'lifo']) - .optional() - .describe('Loading/unloading policy') -}); - -const serviceSchema = z.object({ - name: z.string().describe('Unique identifier for this service'), - location: z - .string() - .describe( - 'Location as a STRING name where service is performed. Use "location-0" for coordinates[0], "location-1" for coordinates[1], etc. (NOT integers like 0, 1, 2)' - ), - duration: z - .number() - .int() - .nonnegative() - .optional() - .default(0) - .describe('Service duration in seconds'), - requirements: z - .array(z.string()) - .optional() - .describe('Required vehicle capabilities'), - service_times: z - .array(timeWindowSchema) - .optional() - .describe('Time window constraints for service') -}); - -const shipmentSchema = z.object({ - name: z.string().describe('Unique identifier for this shipment'), - from: z - .string() - .describe( - 'Pickup location as a STRING name (e.g., "location-0" for coordinates[0], NOT the integer 0)' - ), - to: z - .string() - .describe( - 'Delivery location as a STRING name (e.g., "location-0" for coordinates[0], NOT the integer 0)' - ), - size: z - .record(z.string(), z.number()) - .optional() - .describe('Capacity consumption (weight, volume, etc.)'), - requirements: z - .array(z.string()) - .optional() - .describe('Required vehicle capabilities'), - pickup_duration: z - .number() - .int() - .nonnegative() - .optional() - .default(0) - .describe('Pickup duration in seconds'), - dropoff_duration: z - .number() - .int() - .nonnegative() - .optional() - .default(0) - .describe('Dropoff duration in seconds'), - pickup_times: z - .array(timeWindowSchema) - .optional() - .describe('Time window constraints for pickup'), - dropoff_times: z - .array(timeWindowSchema) - .optional() - .describe('Time window constraints for dropoff') -}); - -export const OptimizationV2InputSchema = z.object({ - coordinates: z - .array(coordinateSchema) - .min(2, 'At least 2 coordinates are required') - .max(1000, 'Maximum 1000 coordinates allowed') - .describe( - 'Array of {longitude, latitude} coordinate pairs to optimize a route through. ' + - 'Must include at least 2 coordinate pairs. ' + - 'Up to 1000 coordinates total are supported.' - ), - profile: profileSchema - .optional() - .default('mapbox/driving') - .describe('Routing profile to use for optimization'), - vehicles: z - .array(vehicleSchema) - .optional() - .describe( - 'Array of vehicle objects. If not provided, a default vehicle will be created automatically. ' + - 'IMPORTANT: If you provide vehicles, you MUST also provide either "services" or "shipments" to define the stops each vehicle should visit.' - ), - services: z - .array(serviceSchema) - .optional() - .describe( - 'Array of service objects representing individual stops/tasks (e.g., deliveries, pickups, visits). ' + - 'Each service is performed at a single location. Required when using custom vehicles.' - ), - shipments: z - .array(shipmentSchema) - .optional() - .describe( - 'Array of shipment objects representing paired pickup/delivery tasks. ' + - 'Each shipment has a pickup location and a delivery location. Can be used with or instead of services when using custom vehicles.' - ), - options: optionsSchema.optional().describe('Optimization options'), - max_polling_attempts: z - .number() - .int() - .positive() - .optional() - .default(30) - .describe('Maximum number of polling attempts (default: 30)'), - polling_interval_ms: z - .number() - .int() - .positive() - .optional() - .default(1000) - .describe('Polling interval in milliseconds (default: 1000)') -}); - -export type OptimizationV2Input = z.infer; diff --git a/src/tools/optimization-tool/OptimizationV2Tool.output.schema.ts b/src/tools/optimization-tool/OptimizationV2Tool.output.schema.ts deleted file mode 100644 index 2c711cf6..00000000 --- a/src/tools/optimization-tool/OptimizationV2Tool.output.schema.ts +++ /dev/null @@ -1,139 +0,0 @@ -// Copyright (c) Mapbox, Inc. -// Licensed under the MIT License. - -import { z } from 'zod'; - -/** - * Stop schema for route stops - * Made flexible to handle various API response formats - */ -const stopSchema = z - .object({ - type: z - .string() - .describe( - 'Type of stop. Known values include "start" (vehicle begins), "service" (perform service), "pickup"/"dropoff" (shipment handling), "break" (vehicle break), "end" (vehicle finishes). Additional values may be returned as the API evolves.' - ), - location: z - .string() - .describe( - 'Location name for this stop (e.g., "location-0", "location-1")' - ), - eta: z - .string() - .optional() - .describe( - 'Estimated time of arrival in ISO 8601 format (e.g., "2023-07-22T14:29:23-07:00")' - ), - odometer: z - .number() - .optional() - .describe('Total distance traveled to reach this stop in meters'), - wait: z - .number() - .optional() - .describe('Wait time at this stop in seconds before proceeding'), - duration: z - .number() - .optional() - .describe('Duration of service/activity at this stop in seconds'), - services: z - .array(z.string()) - .optional() - .describe('Array of service names fulfilled at this stop'), - pickups: z - .array(z.string()) - .optional() - .describe('Array of shipment names picked up at this stop'), - dropoffs: z - .array(z.string()) - .optional() - .describe('Array of shipment names dropped off at this stop') - }) - .passthrough(); // Allow additional fields from API - -/** - * Route schema - */ -const routeSchema = z - .object({ - vehicle: z - .string() - .describe( - 'Vehicle name for this route (matches the vehicle name from input)' - ), - stops: z - .array(stopSchema) - .describe( - 'Ordered sequence of stops for this vehicle, from start to end. Includes all services, pickups, dropoffs, and breaks in optimized order.' - ) - }) - .passthrough(); // Allow additional fields from API - -/** - * Dropped items schema - */ -const droppedSchema = z - .object({ - services: z - .array(z.string()) - .optional() - .describe( - 'Array of service names that could not be fulfilled due to constraints (e.g., time windows, capacity, capabilities)' - ), - shipments: z - .array(z.string()) - .optional() - .describe( - 'Array of shipment names that could not be fulfilled due to constraints (e.g., time windows, capacity, capabilities)' - ) - }) - .passthrough(); // Allow additional fields from API - -/** - * Output schema for OptimizationV2Tool (V2 API - Beta) - * Uses passthrough to be flexible with API response variations - */ -export const OptimizationV2OutputSchema = z - .object({ - version: z - .number() - .optional() - .describe('API version number (always 1 for Optimization API v2)'), - dropped: droppedSchema - .optional() - .describe( - 'Items that could not be fulfilled in the optimization. If present, contains arrays of service/shipment names that were dropped.' - ), - routes: z - .array(routeSchema) - .optional() - .describe( - 'Array of optimized routes, one per vehicle. Each route contains the vehicle name and an ordered sequence of stops. May be undefined if optimization fails completely.' - ), - // Error response fields from API - code: z - .string() - .optional() - .describe( - 'Error code from API (e.g., "internal_error"). Present only when the API returns an error response.' - ), - message: z - .string() - .optional() - .describe( - 'Human-readable error message from API. Present only when the API returns an error response.' - ), - ref: z - .string() - .optional() - .describe( - 'Reference ID for the error from API. Present only when the API returns an error response.' - ) - }) - .passthrough(); // Allow additional fields from API - -/** - * Type inference for OptimizationV2Output - */ -export type OptimizationV2Output = z.infer; diff --git a/src/tools/optimization-tool/OptimizationV2Tool.ts b/src/tools/optimization-tool/OptimizationV2Tool.ts deleted file mode 100644 index 643563b2..00000000 --- a/src/tools/optimization-tool/OptimizationV2Tool.ts +++ /dev/null @@ -1,442 +0,0 @@ -// Copyright (c) Mapbox, Inc. -// Licensed under the MIT License. - -import type { z } from 'zod'; -import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import type { HttpRequest } from '../../utils/types.js'; -import type { - TaskStore, - CreateTaskRequestHandlerExtra, - TaskRequestHandlerExtra -} from '@modelcontextprotocol/sdk/experimental/tasks/interfaces.js'; -import { OptimizationV2InputSchema } from './OptimizationV2Tool.input.schema.js'; -import { - OptimizationV2OutputSchema, - type OptimizationV2Output -} from './OptimizationV2Tool.output.schema.js'; - -/** - * OptimizationV2Tool - Advanced route optimization using Mapbox Optimization API V2 (BETA) - * - * ⚠️ IMPORTANT: This tool uses the V2 API which is currently in beta and requires early access. - * The tool is not registered by default. To enable it, add it to the toolRegistry. - * - * V2 Features: - * - Time window constraints for services and shipments - * - Vehicle capacity limits for multiple resource types - * - Driver shifts with earliest start and latest end times - * - Pickup and dropoff constraints for shipment handling - * - Vehicle capabilities requirements (e.g., refrigeration, ladder) - * - Loading policies (FIFO, LIFO, or any order) - * - Mandated breaks for vehicles - * - * API documentation: https://docs.mapbox.com/api/navigation/optimization/ - */ - -/** - * Helper function to validate JWT format - */ -function isValidJwtFormat(token: string): boolean { - const parts = token.split('.'); - if (parts.length !== 3) return false; - return parts.every((part) => part.length > 0); -} - -/** - * Helper function to get Mapbox API endpoint - */ -function getMapboxApiEndpoint(): string { - return process.env.MAPBOX_API_ENDPOINT || 'https://api.mapbox.com/'; -} - -/** - * Core function to submit optimization job to Mapbox API - */ -async function submitOptimizationJob( - input: z.infer, - accessToken: string, - httpRequest: HttpRequest -): Promise<{ jobId: string; status: string }> { - // Validate: If vehicles are provided, at least one service or shipment is required - if (input.vehicles && input.vehicles.length > 0) { - const hasServices = input.services && input.services.length > 0; - const hasShipments = input.shipments && input.shipments.length > 0; - - if (!hasServices && !hasShipments) { - throw new Error( - 'When vehicles are provided, at least one service or shipment is required' - ); - } - } - - // Convert simplified input to Optimization API v2 format - // Create locations array with auto-generated names - const locations = input.coordinates.map((coord, index) => ({ - name: `location-${index}`, - coordinates: [coord.longitude, coord.latitude] as [number, number] - })); - - // If vehicles are not provided, create a default vehicle - let vehicles: Array; - let services: Array; - let shipments: Array | undefined; - - if (!input.vehicles || input.vehicles.length === 0) { - // Simplified mode: auto-generate vehicle and services from coordinates - vehicles = [ - { - name: 'vehicle-1', - routing_profile: input.profile, - start_location: 'location-0', // Start at first location - end_location: 'location-0' // Return to first location - } - ]; - - // Create services for all locations except the first (which is start/end) - services = input.coordinates.slice(1).map((_, index) => ({ - name: `service-${index + 1}`, - location: `location-${index + 1}`, - duration: 0 // No service time by default - })); - } else { - // Advanced mode: use provided vehicles, services, and shipments - vehicles = input.vehicles; - services = input.services || []; - shipments = input.shipments; - } - - // Build request body - const requestBody: { - version: number; - locations: Array<{ name: string; coordinates: [number, number] }>; - vehicles: Array; - services: Array; - shipments?: Array; - } = { - version: 1, - locations, - vehicles, - services - }; - - if (shipments && shipments.length > 0) { - requestBody.shipments = shipments; - } - - // Step 1: POST to create optimization job - const postUrl = `${getMapboxApiEndpoint()}optimized-trips/v2?access_token=${accessToken}`; - - const postResponse = await httpRequest(postUrl, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(requestBody) - }); - - if (!postResponse.ok) { - const errorText = await postResponse.text(); - let errorMessage = `Request failed with status ${postResponse.status}: ${postResponse.statusText}`; - - try { - const errorJson = JSON.parse(errorText); - if (errorJson.message) { - errorMessage = `${errorMessage} - ${errorJson.message}`; - } - } catch { - // If parsing fails, use the raw text - if (errorText) { - errorMessage = `${errorMessage} - ${errorText}`; - } - } - - throw new Error(errorMessage); - } - - const postData = (await postResponse.json()) as { - id: string; - status: string; - }; - - return { - jobId: postData.id, - status: postData.status - }; -} - -/** - * Core function to poll optimization job status - */ -async function pollOptimizationJob( - jobId: string, - accessToken: string, - httpRequest: HttpRequest -): Promise<{ status: number; data?: OptimizationV2Output; error?: string }> { - const getUrl = `${getMapboxApiEndpoint()}optimized-trips/v2/${jobId}?access_token=${accessToken}`; - const getResponse = await httpRequest(getUrl); - - if (!getResponse.ok) { - if (getResponse.status === 404) { - return { - status: 404, - error: `Optimization job ${jobId} not found` - }; - } - - return { - status: getResponse.status, - error: `Request failed with status ${getResponse.status}: ${getResponse.statusText}` - }; - } - - // HTTP 200 means the optimization is complete - if (getResponse.status === 200) { - const result = (await getResponse.json()) as OptimizationV2Output; - return { - status: 200, - data: result - }; - } - - // HTTP 202 means still processing - return { - status: 202 - }; -} - -/** - * Background polling function that runs until job completes - */ -async function backgroundPollOptimizationJob( - jobId: string, - accessToken: string, - httpRequest: HttpRequest, - taskStore: TaskStore, - taskId: string, - maxAttempts: number, - pollingInterval: number -): Promise { - let attempts = 0; - - while (attempts < maxAttempts) { - try { - const pollResult = await pollOptimizationJob( - jobId, - accessToken, - httpRequest - ); - - if (pollResult.status === 200 && pollResult.data) { - // Job completed successfully - try { - const validatedData = OptimizationV2OutputSchema.parse( - pollResult.data - ); - - await taskStore.storeTaskResult(taskId, 'completed', { - content: [ - { type: 'text', text: JSON.stringify(validatedData, null, 2) } - ], - structuredContent: validatedData - }); - } catch { - // If validation fails, return raw result anyway - await taskStore.storeTaskResult(taskId, 'completed', { - content: [ - { type: 'text', text: JSON.stringify(pollResult.data, null, 2) } - ], - structuredContent: pollResult.data - }); - } - return; - } else if (pollResult.error) { - // Job failed or not found - await taskStore.storeTaskResult(taskId, 'failed', { - content: [{ type: 'text', text: pollResult.error }], - isError: true - }); - return; - } - - // Still processing (status 202), update task status - await taskStore.updateTaskStatus( - taskId, - 'working', - `Processing optimization job (attempt ${attempts + 1}/${maxAttempts})` - ); - - // Wait before next poll - await new Promise((resolve) => setTimeout(resolve, pollingInterval)); - attempts++; - } catch (error) { - // Error during polling - await taskStore.storeTaskResult(taskId, 'failed', { - content: [ - { - type: 'text', - text: `Error polling optimization job: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }); - return; - } - } - - // Timed out - await taskStore.storeTaskResult(taskId, 'failed', { - content: [ - { - type: 'text', - text: `Optimization timed out after ${maxAttempts} attempts. Job ID: ${jobId}. You can check the status later using the job ID.` - } - ], - isError: true - }); -} - -/** - * Register the Optimization V2 tool as a task-based tool - * - * ⚠️ BETA: This tool uses the V2 API which requires early access. - * Not registered by default - call this function manually to enable. - */ -export function registerOptimizationV2Task( - server: McpServer, - httpRequest: HttpRequest -): void { - server.experimental.tasks.registerToolTask( - 'optimization_v2_tool', - { - title: 'Optimization Tool V2 (Beta)', - description: - 'Solves vehicle routing problems using the Mapbox Optimization API v2 (Beta). ' + - 'This is a long-running async task that submits an optimization job and polls for results. ' + - 'Supports up to 1000 coordinates in {longitude, latitude} format. ' + - 'Returns optimized routes with stops, ETAs, and dropped items. ' + - '\n\n' + - 'USAGE MODES:\n' + - '1. SIMPLIFIED MODE (recommended for basic routing): Provide only "coordinates" and optionally "profile". ' + - 'The tool will automatically create a default vehicle that visits all locations in optimized order, ' + - 'starting and ending at the first coordinate.\n' + - '2. ADVANCED MODE (for complex scenarios): If you specify "vehicles", you MUST also provide either ' + - '"services" (single stops like deliveries or pickups) or "shipments" (paired pickup/dropoff tasks). ' + - 'Each service/shipment defines a stop that vehicles must visit.\n' + - '\n' + - 'KEY CONCEPTS:\n' + - '- "services": Individual stops at locations (e.g., deliver package to address A, pick up at coffee shop B)\n' + - '- "shipments": Paired pickup/delivery tasks (e.g., pick up from warehouse, deliver to customer)\n' + - '- "vehicles": Routing agents with optional constraints like time windows, capacities, and capabilities\n' + - '\n' + - 'LOCATION NAMING (CRITICAL):\n' + - 'The tool auto-generates STRING location names from your coordinates array as "location-0", "location-1", "location-2", etc. ' + - 'When creating services, shipments, or vehicles, you MUST reference these auto-generated STRINGS (NOT integers, NOT array indices):\n' + - '- coordinates[0] → use STRING "location-0" (NOT integer 0)\n' + - '\n' + - 'EXAMPLES:\n' + - '✓ CORRECT: service.location = "location-2" (string)\n' + - '✗ WRONG: service.location = 2 (integer)\n' + - '\n' + - 'IMPORTANT: This is an async task. The task will start immediately but results will be available later via task polling.', - inputSchema: OptimizationV2InputSchema, - outputSchema: OptimizationV2OutputSchema, - annotations: { - title: 'Optimization Tool', - readOnlyHint: true, - destructiveHint: false, - idempotentHint: false, - openWorldHint: true - } - }, - { - async createTask(args: unknown, extra: CreateTaskRequestHandlerExtra) { - // Validate input - const input = OptimizationV2InputSchema.parse(args); - - // Get and validate access token (from environment only for now) - const accessToken = - process.env.MAPBOX_ACCESS_TOKEN || - ( - extra as CreateTaskRequestHandlerExtra & { - authInfo?: { token?: string }; - } - ).authInfo?.token; - if (!accessToken) { - throw new Error( - 'No access token available. Please provide via Bearer auth or MAPBOX_ACCESS_TOKEN env var' - ); - } - if (!isValidJwtFormat(accessToken)) { - throw new Error('Access token is not in valid JWT format'); - } - - // Create the task - const task = await extra.taskStore.createTask({ - ttl: 300000 // 5 minutes TTL - }); - - // Submit optimization job to Mapbox API - try { - const { jobId } = await submitOptimizationJob( - input, - accessToken, - httpRequest - ); - - // Start background polling (fire and forget) - const maxAttempts = input.max_polling_attempts ?? 10; - const pollingInterval = input.polling_interval_ms ?? 1000; - - // Don't await this - let it run in the background - void backgroundPollOptimizationJob( - jobId, - accessToken, - httpRequest, - extra.taskStore, - task.taskId, - maxAttempts, - pollingInterval - ); - - // Update task status to show it's working - await extra.taskStore.updateTaskStatus( - task.taskId, - 'working', - `Optimization job submitted: ${jobId}` - ); - - // Return CreateTaskResult with the created task - return { - task - }; - } catch (error) { - // Failed to submit job, mark task as failed - await extra.taskStore.storeTaskResult(task.taskId, 'failed', { - content: [ - { - type: 'text', - text: `Failed to submit optimization job: ${error instanceof Error ? error.message : String(error)}` - } - ], - isError: true - }); - - throw error; - } - }, - - async getTask(_args: unknown, extra: TaskRequestHandlerExtra) { - const task = await extra.taskStore.getTask(extra.taskId); - if (!task) { - throw new Error(`Task ${extra.taskId} not found`); - } - return task; - }, - - async getTaskResult(_args: unknown, extra: TaskRequestHandlerExtra) { - const result = await extra.taskStore.getTaskResult(extra.taskId); - // taskStore.getTaskResult returns a Result type, but we know we stored a CallToolResult - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return result as any; - } - } - ); -} diff --git a/src/tools/place-details-tool/PlaceDetailsTool.ts b/src/tools/place-details-tool/PlaceDetailsTool.ts index 8d6d3af2..5226d6b6 100644 --- a/src/tools/place-details-tool/PlaceDetailsTool.ts +++ b/src/tools/place-details-tool/PlaceDetailsTool.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 type { ToolExecutionContext } from '../../utils/tracing.js'; import { PlaceDetailsInputSchema } from './PlaceDetailsTool.input.schema.js'; diff --git a/src/tools/points-within-polygon-tool/PointsWithinPolygonTool.ts b/src/tools/points-within-polygon-tool/PointsWithinPolygonTool.ts index e9452cfd..9aac0308 100644 --- a/src/tools/points-within-polygon-tool/PointsWithinPolygonTool.ts +++ b/src/tools/points-within-polygon-tool/PointsWithinPolygonTool.ts @@ -15,7 +15,7 @@ import { PointsWithinPolygonOutputSchema, type PointsWithinPolygonOutput } from './PointsWithinPolygonTool.output.schema.js'; -import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; +import type { CallToolResult } from '@modelcontextprotocol/server'; export class PointsWithinPolygonTool extends BaseTool< typeof PointsWithinPolygonInputSchema, diff --git a/src/tools/render-map-tool/RenderMapTool.ts b/src/tools/render-map-tool/RenderMapTool.ts index e3a5eebe..16500299 100644 --- a/src/tools/render-map-tool/RenderMapTool.ts +++ b/src/tools/render-map-tool/RenderMapTool.ts @@ -2,14 +2,16 @@ // Licensed under the MIT License. import { context, SpanStatusCode, trace } from '@opentelemetry/api'; -import type { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol.js'; +import type { + CallToolResult, + ServerContext +} from '@modelcontextprotocol/server'; import { createLocalToolExecutionContext } from '../../utils/tracing.js'; import { BaseTool } from '../BaseTool.js'; import { mapAppCsp, type MapAppCsp } from '../../resources/ui-apps/mapAppCsp.js'; -import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; import { RenderMapInputSchema, type RenderMapInput @@ -114,11 +116,11 @@ export class RenderMapTool extends BaseTool< async run( rawInput: unknown, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - extra?: RequestHandlerExtra + + extra?: ServerContext ): Promise { const accessToken = - extra?.authInfo?.token || process.env.MAPBOX_ACCESS_TOKEN; + extra?.http?.authInfo?.token || process.env.MAPBOX_ACCESS_TOKEN; const owner = getOwnerKeyFromToken(accessToken); const toolContext = createLocalToolExecutionContext(this.name, 0); return await context.with( diff --git a/src/tools/resource-reader-tool/ResourceReaderTool.ts b/src/tools/resource-reader-tool/ResourceReaderTool.ts index dc71c560..113bbb05 100644 --- a/src/tools/resource-reader-tool/ResourceReaderTool.ts +++ b/src/tools/resource-reader-tool/ResourceReaderTool.ts @@ -2,8 +2,10 @@ // Licensed under the MIT License. import { BaseTool } from '../BaseTool.js'; -import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; -import type { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol.js'; +import type { + CallToolResult, + ServerContext +} from '@modelcontextprotocol/server'; import type { ResourceReaderToolInput } from './ResourceReaderTool.input.schema.js'; import { ResourceReaderToolInputSchema } from './ResourceReaderTool.input.schema.js'; import { ResourceReaderToolOutputSchema } from './ResourceReaderTool.output.schema.js'; @@ -37,8 +39,8 @@ export class ResourceReaderTool extends BaseTool< async run( rawInput: unknown, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - extra?: RequestHandlerExtra + + extra?: ServerContext ): Promise { try { const input = this.inputSchema.parse(rawInput); @@ -72,8 +74,8 @@ export class ResourceReaderTool extends BaseTool< private async execute( input: ResourceReaderToolInput, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - extra?: RequestHandlerExtra + + extra?: ServerContext ): Promise { // Find the resource by URI const resource = getResourceByUri(input.uri); diff --git a/src/tools/reverse-geocode-tool/ReverseGeocodeTool.ts b/src/tools/reverse-geocode-tool/ReverseGeocodeTool.ts index 50634ecb..ef3d9aa6 100644 --- a/src/tools/reverse-geocode-tool/ReverseGeocodeTool.ts +++ b/src/tools/reverse-geocode-tool/ReverseGeocodeTool.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 { ReverseGeocodeInputSchema } from './ReverseGeocodeTool.input.schema.js'; import { GeocodingResponseSchema } from './ReverseGeocodeTool.output.schema.js'; diff --git a/src/tools/search-and-geocode-tool/SearchAndGeocodeTool.ts b/src/tools/search-and-geocode-tool/SearchAndGeocodeTool.ts index 999342ba..277651c7 100644 --- a/src/tools/search-and-geocode-tool/SearchAndGeocodeTool.ts +++ b/src/tools/search-and-geocode-tool/SearchAndGeocodeTool.ts @@ -14,7 +14,7 @@ import type { MapboxFeatureCollection, MapboxFeature } from '../../schemas/geojson.js'; -import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; +import type { CallToolResult } from '@modelcontextprotocol/server'; import { buildSearchAndGeocodeRequestUrl } from './buildSearchAndGeocodeRequestUrl.js'; import { renderHint } from '../../utils/storeMapPayload.js'; import { buildSelfFetchRef } from '../../utils/selfFetchRef.js'; diff --git a/src/tools/simplify-tool/SimplifyTool.ts b/src/tools/simplify-tool/SimplifyTool.ts index 24a4a490..55c52684 100644 --- a/src/tools/simplify-tool/SimplifyTool.ts +++ b/src/tools/simplify-tool/SimplifyTool.ts @@ -10,7 +10,7 @@ import { SimplifyOutputSchema, type SimplifyOutput } from './SimplifyTool.output.schema.js'; -import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; +import type { CallToolResult } from '@modelcontextprotocol/server'; /** * SimplifyTool - Simplify geometries by reducing vertex count diff --git a/src/tools/static-map-image-tool/StaticMapImageTool.ts b/src/tools/static-map-image-tool/StaticMapImageTool.ts index e444e59d..37729d21 100644 --- a/src/tools/static-map-image-tool/StaticMapImageTool.ts +++ b/src/tools/static-map-image-tool/StaticMapImageTool.ts @@ -7,7 +7,7 @@ import { MapboxApiBasedTool } from '../MapboxApiBasedTool.js'; import type { HttpRequest } from '../../utils/types.js'; import { StaticMapImageInputSchema } from './StaticMapImageTool.input.schema.js'; import type { OverlaySchema } from './StaticMapImageTool.input.schema.js'; -import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; +import type { CallToolResult } from '@modelcontextprotocol/server'; import { temporaryResourceManager } from '../../utils/temporaryResourceManager.js'; import { getOwnerKeyFromToken } from '../../utils/jwtUtils.js'; diff --git a/src/tools/union-tool/UnionTool.ts b/src/tools/union-tool/UnionTool.ts index 5985da68..bec01c1d 100644 --- a/src/tools/union-tool/UnionTool.ts +++ b/src/tools/union-tool/UnionTool.ts @@ -10,7 +10,7 @@ import { UnionOutputSchema, type UnionOutput } from './UnionTool.output.schema.js'; -import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; +import type { CallToolResult } from '@modelcontextprotocol/server'; import { renderHint } from '../../utils/storeMapPayload.js'; import { buildComputeRef } from '../../utils/computeRef.js'; diff --git a/src/tools/validateOutput.ts b/src/tools/validateOutput.ts index bf4f8bfd..3a8aaf51 100644 --- a/src/tools/validateOutput.ts +++ b/src/tools/validateOutput.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. import type { ZodTypeAny } from 'zod'; -import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; +import type { CallToolResult } from '@modelcontextprotocol/server'; /** * Validates the structured content against the output schema. diff --git a/src/utils/index.ts b/src/utils/index.ts index e24863c3..3a28b6e8 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -58,8 +58,8 @@ export type { VersionInfo } from './versionUtils.js'; // Export server setup helpers for consumers building their own McpServer // (e.g. a hosted deployment) rather than running this package's entry point. -// Call publishJsonSchema2020(server) before installing any tool, and pass -// buildServerInstructions() as the server's instructions. +// Pass buildServerInstructions() as the server's +// instructions. publishJsonSchema2020 is a deprecated no-op on SDK v2. export { publishJsonSchema2020, toJsonSchema2020 } from './jsonSchema2020.js'; export { buildServerInstructions } from './serverInstructions.js'; diff --git a/src/utils/jsonSchema2020.ts b/src/utils/jsonSchema2020.ts index 1b9d93b4..b1ba04d8 100644 --- a/src/utils/jsonSchema2020.ts +++ b/src/utils/jsonSchema2020.ts @@ -1,8 +1,7 @@ // Copyright (c) Mapbox, Inc. // Licensed under the MIT License. -import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import { ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'; +import type { McpServer } from '@modelcontextprotocol/server'; export const JSON_SCHEMA_2020_12 = 'https://json-schema.org/draft/2020-12/schema'; @@ -104,46 +103,14 @@ export function toJsonSchema2020(schema: unknown): unknown { } /** - * Make `tools/list` publish JSON Schema 2020-12 instead of draft-07. + * No-op, kept so callers written for SDK v1 keep working. * - * MCP SDK 1.x's McpServer generates every tool's inputSchema/outputSchema - * as draft-07 (with no option to change it), but MCP specifies 2020-12 as - * the default dialect, and newer clients enforce that — Claude Desktop - * refuses to call any tool whose outputSchema declares draft-07 ("JSON - * Schema declares an unsupported dialect ... The default validator - * supports JSON Schema 2020-12 only"). + * MCP SDK v1's McpServer published every tool schema as draft-07, which + * newer clients reject (Claude Desktop: "JSON Schema declares an + * unsupported dialect"), so this used to convert `tools/list` schemas to + * 2020-12. SDK v2 publishes 2020-12 itself. * - * McpServer installs its tools/list handler through the public - * setRequestHandler on first tool registration, so this wraps that method - * on this one server instance (not a global patch) and converts the - * schemas in the handler's result. Must be called before any tool is - * registered. + * @deprecated Not needed on SDK v2; remove the call. */ -export function publishJsonSchema2020(server: McpServer): void { - const inner = server.server; - const listToolsMethod = ListToolsRequestSchema.shape.method.value; - const original = inner.setRequestHandler.bind(inner); - - // setRequestHandler's generic signature can't be expressed for a wrapper - // without re-declaring the SDK's internal schema types. - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (inner as any).setRequestHandler = (requestSchema: any, handler: any) => { - if (requestSchema?.shape?.method?.value !== listToolsMethod) { - return original(requestSchema, handler); - } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return original(requestSchema, async (request: any, extra: any) => { - const result = await handler(request, extra); - return { - ...result, - tools: result.tools.map((tool: Schema) => ({ - ...tool, - inputSchema: toJsonSchema2020(tool.inputSchema), - ...(tool.outputSchema !== undefined && { - outputSchema: toJsonSchema2020(tool.outputSchema) - }) - })) - }; - }); - }; -} +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export function publishJsonSchema2020(_server: McpServer): void {} diff --git a/src/utils/versionUtils-cjs.cjs b/src/utils/versionUtils-cjs.cjs index 7db3f24b..083500f2 100644 --- a/src/utils/versionUtils-cjs.cjs +++ b/src/utils/versionUtils-cjs.cjs @@ -1,48 +1,58 @@ -"use strict"; +'use strict'; // Copyright (c) Mapbox, Inc. // Licensed under the MIT License. -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); +var __importDefault = + (this && this.__importDefault) || + function (mod) { + return mod && mod.__esModule ? mod : { default: mod }; + }; +Object.defineProperty(exports, '__esModule', { value: true }); exports.getVersionInfo = getVersionInfo; -const node_fs_1 = require("node:fs"); -const node_path_1 = __importDefault(require("node:path")); +const node_fs_1 = require('node:fs'); +const node_path_1 = __importDefault(require('node:path')); function getVersionInfo() { - const name = 'Mapbox MCP server'; + const name = 'Mapbox MCP server'; + try { + const dirname = __dirname; + // Try to read from version.json first (for build artifacts) + const versionJsonPath = node_path_1.default.resolve( + dirname, + '..', + 'version.json' + ); try { - const dirname = __dirname; - // Try to read from version.json first (for build artifacts) - const versionJsonPath = node_path_1.default.resolve(dirname, '..', 'version.json'); - try { - const versionData = (0, node_fs_1.readFileSync)(versionJsonPath, 'utf-8'); - const info = JSON.parse(versionData); - info['name'] = name; - return info; - } - catch { - // Fall back to package.json - const packageJsonPath = node_path_1.default.resolve(dirname, '..', '..', '..', 'package.json'); - const packageData = (0, node_fs_1.readFileSync)(packageJsonPath, 'utf-8'); - const packageInfo = JSON.parse(packageData); - return { - name: name, - version: packageInfo.version || '0.0.0', - sha: 'unknown', - tag: 'unknown', - branch: 'unknown' - }; - } - } - catch (error) { - console.warn(`Failed to read version info: ${error}`); - return { - name: name, - version: '0.0.0', - sha: 'unknown', - tag: 'unknown', - branch: 'unknown' - }; + const versionData = (0, node_fs_1.readFileSync)(versionJsonPath, 'utf-8'); + const info = JSON.parse(versionData); + info['name'] = name; + return info; + } catch { + // Fall back to package.json + const packageJsonPath = node_path_1.default.resolve( + dirname, + '..', + '..', + '..', + 'package.json' + ); + const packageData = (0, node_fs_1.readFileSync)(packageJsonPath, 'utf-8'); + const packageInfo = JSON.parse(packageData); + return { + name: name, + version: packageInfo.version || '0.0.0', + sha: 'unknown', + tag: 'unknown', + branch: 'unknown' + }; } + } catch (error) { + console.warn(`Failed to read version info: ${error}`); + return { + name: name, + version: '0.0.0', + sha: 'unknown', + tag: 'unknown', + branch: 'unknown' + }; + } } -//# sourceMappingURL=versionUtils-cjs.cjs.map \ No newline at end of file +//# sourceMappingURL=versionUtils-cjs.cjs.map diff --git a/src/utils/versionUtils-cjs.d.cts b/src/utils/versionUtils-cjs.d.cts index 3f068751..a5985e15 100644 --- a/src/utils/versionUtils-cjs.d.cts +++ b/src/utils/versionUtils-cjs.d.cts @@ -1,9 +1,9 @@ export interface VersionInfo { - name: string; - version: string; - sha: string; - tag: string; - branch: string; + name: string; + version: string; + sha: string; + tag: string; + branch: string; } export declare function getVersionInfo(): VersionInfo; -//# sourceMappingURL=versionUtils-cjs.d.cts.map \ No newline at end of file +//# sourceMappingURL=versionUtils-cjs.d.cts.map diff --git a/test/exports.test.ts b/test/exports.test.ts index 6095c63f..c177c0ae 100644 --- a/test/exports.test.ts +++ b/test/exports.test.ts @@ -183,12 +183,10 @@ describe('Package exports', () => { // Mirrors how an external deployment (e.g. hosted-mcp-server) builds its // own McpServer from the subpath exports instead of running src/index.ts. it('should give an externally built server 2020-12 schemas and instructions', async () => { - const { McpServer } = - await import('@modelcontextprotocol/sdk/server/mcp.js'); - const { Client } = - await import('@modelcontextprotocol/sdk/client/index.js'); + const { McpServer } = await import('@modelcontextprotocol/server'); + const { Client } = await import('@modelcontextprotocol/client'); const { InMemoryTransport } = - await import('@modelcontextprotocol/sdk/inMemory.js'); + await import('@modelcontextprotocol/server'); const { publishJsonSchema2020, buildServerInstructions } = await import('../src/utils/index.js'); const { getAllTools } = await import('../src/tools/index.js'); diff --git a/test/integration/hostedMultiTaskFollowup.test.ts b/test/integration/hostedMultiTaskFollowup.test.ts index f7fdbfc9..271e48d0 100644 --- a/test/integration/hostedMultiTaskFollowup.test.ts +++ b/test/integration/hostedMultiTaskFollowup.test.ts @@ -6,8 +6,8 @@ import { existsSync } from 'node:fs'; import { join, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; import { createServer, type Server } from 'node:http'; -import { Client } from '@modelcontextprotocol/sdk/client/index.js'; -import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; +import { StdioClientTransport } from '@modelcontextprotocol/client/stdio'; +import { Client } from '@modelcontextprotocol/client'; /** * Reproduces the hosted deployment's actual failure mode: a large diff --git a/test/integration/processRestart.test.ts b/test/integration/processRestart.test.ts index 774dd60a..208e50ad 100644 --- a/test/integration/processRestart.test.ts +++ b/test/integration/processRestart.test.ts @@ -5,8 +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 { StdioClientTransport } from '@modelcontextprotocol/client/stdio'; +import { Client } from '@modelcontextprotocol/client'; /** * Spawns the *actual built server* (dist/esm/index.js — the same entry point diff --git a/test/patches/mcp-sdk-patch.test.ts b/test/patches/mcp-sdk-patch.test.ts deleted file mode 100644 index 6d9cd43f..00000000 --- a/test/patches/mcp-sdk-patch.test.ts +++ /dev/null @@ -1,282 +0,0 @@ -// Copyright (c) Mapbox, Inc. -// Licensed under the MIT License. - -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import { z } from 'zod'; -import { BaseTool } from '../../src/tools/BaseTool.js'; -import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; -import type { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol.js'; -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import type { ToolAnnotations } from '@modelcontextprotocol/sdk/types.js'; - -// Define test schemas -const TestInputSchema = z.object({ - input: z.string() -}); - -const TestOutputSchema = z.object({ - name: z.string(), - age: z.number(), - items: z.array(z.string()) // Expects array of strings -}); - -// Create a test tool that returns data violating its output schema -class TestToolWithMismatch extends BaseTool< - typeof TestInputSchema, - typeof TestOutputSchema -> { - readonly name = 'test-mismatch-tool'; - readonly description = 'Tool for testing schema mismatch handling'; - readonly annotations: ToolAnnotations = { - title: 'Test Mismatch Tool', - stabilityHint: 'experimental', - idempotentHint: true, - openWorldHint: false - }; - - constructor() { - super({ - inputSchema: TestInputSchema, - outputSchema: TestOutputSchema - }); - } - - async run( - rawInput: unknown, - extra?: RequestHandlerExtra - ): Promise { - // Parse input (would normally throw if invalid) - const input = this.inputSchema.parse(rawInput); - - // Return data that violates the output schema - return { - content: [{ type: 'text', text: `Processed: ${input.input}` }], - structuredContent: { - name: 'Test User', - age: 30, - items: 'single-string-not-array' // VIOLATION: Should be array! - }, - isError: false - }; - } -} - -// Tool that returns no structured content despite having output schema -class TestToolNoStructuredContent extends BaseTool< - typeof TestInputSchema, - typeof TestOutputSchema -> { - readonly name = 'test-no-structured'; - readonly description = 'Tool that omits structured content'; - readonly annotations: ToolAnnotations = { - title: 'Test No Structured Content', - stabilityHint: 'experimental', - idempotentHint: true, - openWorldHint: false - }; - - constructor() { - super({ - inputSchema: TestInputSchema, - outputSchema: TestOutputSchema - }); - } - - async run( - rawInput: unknown, - extra?: RequestHandlerExtra - ): Promise { - const input = this.inputSchema.parse(rawInput); - - // Return NO structured content despite having output schema - return { - content: [{ type: 'text', text: `Text only: ${input.input}` }], - // Note: no structuredContent field! - isError: false - }; - } -} - -describe('MCP SDK Output Validation Patch', () => { - let server: McpServer; - let consoleWarnSpy: ReturnType; - let consoleErrorSpy: ReturnType; - - beforeEach(() => { - server = new McpServer({ - name: 'test-server', - version: '1.0.0' - }); - - // Spy on console methods - consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); - consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); - }); - - afterEach(() => { - consoleWarnSpy.mockRestore(); - consoleErrorSpy.mockRestore(); - }); - - it('should not throw when tool returns data that does not match output schema', async () => { - const tool = new TestToolWithMismatch(); - - // Register the tool - tool.installTo(server); - - // Execute the tool directly - let result: CallToolResult | undefined; - let errorThrown = false; - - try { - result = await tool.run({ input: 'test-input' }); - } catch (error) { - errorThrown = true; - console.log('Tool execution error:', error); - } - - // The tool itself should execute without error - expect(errorThrown).toBe(false); - expect(result).toBeDefined(); - expect(result!.isError).toBe(false); - expect(result!.structuredContent).toBeDefined(); - - // The mismatched data should still be in the result - expect(result!.structuredContent).toEqual({ - name: 'Test User', - age: 30, - items: 'single-string-not-array' // The invalid value is preserved - }); - }); - - it('should allow tools to return mismatched data without MCP errors', async () => { - // This test verifies that the patch prevents MCP-level validation errors - // We can't easily test the full MCP flow without a real transport, - // but we can verify that the tool executes successfully - const tool = new TestToolWithMismatch(); - tool.installTo(server); - - // The tool should execute without errors - const result = await tool.run({ input: 'test' }); - - // Result should contain the mismatched data - expect(result.isError).toBe(false); - expect(result.structuredContent).toEqual({ - name: 'Test User', - age: 30, - items: 'single-string-not-array' - }); - - // Note: The actual MCP validation happens when the server sends the response - // Our patch makes that validation non-fatal (logs warning instead of throwing) - }); - - it('should handle missing structured content gracefully', async () => { - const tool = new TestToolNoStructuredContent(); - tool.installTo(server); - - // Execute the tool - const result = await tool.run({ input: 'test' }); - - // Should succeed despite missing structured content - expect(result).toBeDefined(); - expect(result.isError).toBe(false); - expect(result.content[0].text).toBe('Text only: test'); - expect(result.structuredContent).toBeUndefined(); - }); - - describe('Patch Detection', () => { - it('should verify the MCP SDK patch is applied by checking the patched file', async () => { - // This test verifies the patch has been applied by checking for our patch marker - - // Read the patched SDK file - const fs = await import('node:fs'); - const path = await import('node:path'); - const { fileURLToPath } = await import('node:url'); - - // Get the SDK file path - const __dirname = path.dirname(fileURLToPath(import.meta.url)); - const sdkPath = path.join( - __dirname, - '..', - '..', - 'node_modules', - '@modelcontextprotocol', - 'sdk', - 'dist', - 'esm', - 'server', - 'mcp.js' - ); - - // Read the file content - const sdkContent = fs.readFileSync(sdkPath, 'utf-8'); - - // Check for our patch markers - const hasPatchMarker = sdkContent.includes('[MCP SDK Patch]'); - const hasWarningInsteadOfThrow = - sdkContent.includes('console.warn') && - sdkContent.includes('validation warning'); - - // Check that validation errors are NOT thrown - const stillThrowsValidationError = - sdkContent.includes('throw new McpError') && - sdkContent.includes('Output validation error'); - - if (!hasPatchMarker || !hasWarningInsteadOfThrow) { - throw new Error( - '🚨 MCP SDK PATCH IS NOT APPLIED! 🚨\n\n' + - 'The patch markers are missing from the SDK file.\n' + - 'This means the patch is not applied.\n\n' + - 'To fix:\n' + - '1. Run: npm run postinstall\n' + - "2. If that doesn't work, check patches/@modelcontextprotocol+sdk+*.patch exists\n" + - '3. If SDK was updated, recreate the patch:\n' + - ' - Modify node_modules/@modelcontextprotocol/sdk/dist/esm/server/mcp.js\n' + - ' - Run: npx patch-package @modelcontextprotocol/sdk' - ); - } - - if (stillThrowsValidationError) { - console.warn( - 'Warning: The SDK still contains throw statements for validation errors.\n' + - 'The patch may be partially applied or incomplete.' - ); - } - - // The patch is applied! - expect(hasPatchMarker).toBe(true); - expect(hasWarningInsteadOfThrow).toBe(true); - }); - - it('should verify patch works at runtime', async () => { - // Runtime verification - tool should work despite schema mismatch - const tool = new TestToolWithMismatch(); - const testServer = new McpServer({ - name: 'runtime-test', - version: '1.0.0' - }); - - // Install tool and verify it doesn't throw during registration - let registrationError: Error | undefined; - try { - tool.installTo(testServer); - } catch (error) { - registrationError = error as Error; - } - - expect(registrationError).toBeUndefined(); - - // Tool should execute successfully despite mismatched output - const result = await tool.run({ input: 'test' }); - expect(result.isError).toBe(false); - expect(result.structuredContent).toBeDefined(); - - // Check that we're getting the mismatched data (not sanitized) - expect(result.structuredContent).toHaveProperty( - 'items', - 'single-string-not-array' - ); - }); - }); -}); diff --git a/test/prompts/BasePrompt.test.ts b/test/prompts/BasePrompt.test.ts index 4d3416d8..67d3869c 100644 --- a/test/prompts/BasePrompt.test.ts +++ b/test/prompts/BasePrompt.test.ts @@ -6,7 +6,7 @@ import { BasePrompt } from '../../src/prompts/BasePrompt.js'; import type { PromptArgument, PromptMessage -} from '@modelcontextprotocol/sdk/types.js'; +} from '@modelcontextprotocol/server'; // Create a concrete test implementation of BasePrompt class TestPrompt extends BasePrompt { diff --git a/test/resources/BaseResource.test.ts b/test/resources/BaseResource.test.ts index 870ed4b7..fded8992 100644 --- a/test/resources/BaseResource.test.ts +++ b/test/resources/BaseResource.test.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. import { describe, it, expect } from 'vitest'; -import type { ReadResourceResult } from '@modelcontextprotocol/sdk/types.js'; +import type { ReadResourceResult } from '@modelcontextprotocol/server'; import { BaseResource } from '../../src/resources/BaseResource.js'; describe('BaseResource', () => { diff --git a/test/resources/compute/ComputeResource.test.ts b/test/resources/compute/ComputeResource.test.ts index 03080671..25af05cf 100644 --- a/test/resources/compute/ComputeResource.test.ts +++ b/test/resources/compute/ComputeResource.test.ts @@ -88,7 +88,7 @@ describe('ComputeResource', () => { ]); // eslint-disable-next-line @typescript-eslint/no-explicit-any - const asAccountB = { authInfo: { token: 'irrelevant' } } as any; + const asAccountB = { http: { authInfo: { token: 'irrelevant' } } } as any; const result = await resource.read(ref, asAccountB); const payload = JSON.parse(result.contents[0].text as string); expect(payload.summary).toBe('Intersection of two polygons'); diff --git a/test/resources/temporary/TemporaryDataResource.test.ts b/test/resources/temporary/TemporaryDataResource.test.ts index 7ab1e5c1..97dfb4b3 100644 --- a/test/resources/temporary/TemporaryDataResource.test.ts +++ b/test/resources/temporary/TemporaryDataResource.test.ts @@ -41,7 +41,7 @@ function forgedTokenFor(username: string): string { // eslint-disable-next-line @typescript-eslint/no-explicit-any function extraFor(token?: string): any { - return token ? { authInfo: { token } } : {}; + return token ? { http: { authInfo: { token } } } : {}; } const NOT_FOUND = diff --git a/test/resources/ui-apps/MapAppUIResource.test.ts b/test/resources/ui-apps/MapAppUIResource.test.ts index 2b626fc0..01eedcdc 100644 --- a/test/resources/ui-apps/MapAppUIResource.test.ts +++ b/test/resources/ui-apps/MapAppUIResource.test.ts @@ -49,7 +49,7 @@ describe('MapAppUIResource', () => { const result = await resource.read('ui://mapbox/map-app/index.html', { // eslint-disable-next-line @typescript-eslint/no-explicit-any - authInfo: { token: SK_TOKEN } as any + http: { authInfo: { token: SK_TOKEN } as any } // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any); @@ -82,7 +82,7 @@ describe('MapAppUIResource', () => { const result = await resource.read('ui://mapbox/map-app/index.html', { // eslint-disable-next-line @typescript-eslint/no-explicit-any - authInfo: { token: SK_TOKEN } as any + http: { authInfo: { token: SK_TOKEN } as any } // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any); diff --git a/test/tools/BaseTool.test.ts b/test/tools/BaseTool.test.ts index dd886ca4..16695379 100644 --- a/test/tools/BaseTool.test.ts +++ b/test/tools/BaseTool.test.ts @@ -4,7 +4,7 @@ import { describe, it, expect, vi } from 'vitest'; import { BaseTool } from '../../src/tools/BaseTool.js'; import { z } from 'zod'; -import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; +import type { CallToolResult } from '@modelcontextprotocol/server'; // Create a concrete test implementation of BaseTool class TestTool extends BaseTool< diff --git a/test/tools/annotations.test.ts b/test/tools/annotations.test.ts index 5c54c44c..68525d3a 100644 --- a/test/tools/annotations.test.ts +++ b/test/tools/annotations.test.ts @@ -3,7 +3,7 @@ import { describe, it, expect } from 'vitest'; import { getAllTools } from '../../src/tools/toolRegistry.js'; -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { McpServer } from '@modelcontextprotocol/server'; describe('Tool Annotations', () => { it('should have annotations for all tools', () => { diff --git a/test/tools/category-search-tool/CategorySearchTool.test.ts b/test/tools/category-search-tool/CategorySearchTool.test.ts index 43c9c16b..7063aefb 100644 --- a/test/tools/category-search-tool/CategorySearchTool.test.ts +++ b/test/tools/category-search-tool/CategorySearchTool.test.ts @@ -469,7 +469,7 @@ describe('CategorySearchTool', () => { proximity: { longitude: -122.42, latitude: 37.78 } }, // eslint-disable-next-line @typescript-eslint/no-explicit-any - { authInfo: { token } } as any + { http: { authInfo: { token } } } as any ); expect(result.isError).toBe(false); diff --git a/test/tools/difference-tool/DifferenceTool.test.ts b/test/tools/difference-tool/DifferenceTool.test.ts index a764d59b..26dc728a 100644 --- a/test/tools/difference-tool/DifferenceTool.test.ts +++ b/test/tools/difference-tool/DifferenceTool.test.ts @@ -203,7 +203,7 @@ describe('DifferenceTool', () => { ] }, // eslint-disable-next-line @typescript-eslint/no-explicit-any - { authInfo: { token } } as any + { http: { authInfo: { token } } } as any ); expect(result.isError).toBe(false); diff --git a/test/tools/directions-tool/DirectionsTool.test.ts b/test/tools/directions-tool/DirectionsTool.test.ts index 6fe88050..1bb1c479 100644 --- a/test/tools/directions-tool/DirectionsTool.test.ts +++ b/test/tools/directions-tool/DirectionsTool.test.ts @@ -1185,7 +1185,7 @@ describe('DirectionsTool', () => { geometries: 'geojson' }, // eslint-disable-next-line @typescript-eslint/no-explicit-any - { authInfo: { token } } as any + { http: { authInfo: { token } } } as any ); expect(result.isError).toBe(false); @@ -1237,7 +1237,7 @@ describe('DirectionsTool', () => { geometries: 'none' }, // eslint-disable-next-line @typescript-eslint/no-explicit-any - { authInfo: { token } } as any + { http: { authInfo: { token } } } as any ); expect(result.isError).toBe(false); @@ -1288,7 +1288,7 @@ describe('DirectionsTool', () => { geometries: 'geojson' }, // eslint-disable-next-line @typescript-eslint/no-explicit-any - { authInfo: { token } } as any + { http: { authInfo: { token } } } as any ); expect(result.isError).toBe(false); diff --git a/test/tools/ground-location-tool/GroundLocationTool.test.ts b/test/tools/ground-location-tool/GroundLocationTool.test.ts index bd076c36..08e2951d 100644 --- a/test/tools/ground-location-tool/GroundLocationTool.test.ts +++ b/test/tools/ground-location-tool/GroundLocationTool.test.ts @@ -331,7 +331,7 @@ describe('GroundLocationTool', () => { query: 'coffee' }, // eslint-disable-next-line @typescript-eslint/no-explicit-any - { authInfo: { token } } as any + { http: { authInfo: { token } } } as any ); expect(result.isError).toBe(false); diff --git a/test/tools/intersect-tool/IntersectTool.test.ts b/test/tools/intersect-tool/IntersectTool.test.ts index 04956072..9e74f4eb 100644 --- a/test/tools/intersect-tool/IntersectTool.test.ts +++ b/test/tools/intersect-tool/IntersectTool.test.ts @@ -203,7 +203,7 @@ describe('IntersectTool', () => { ] }, // eslint-disable-next-line @typescript-eslint/no-explicit-any - { authInfo: { token } } as any + { http: { authInfo: { token } } } as any ); expect(result.isError).toBe(false); diff --git a/test/tools/isochrone-tool/IsochroneTool.test.ts b/test/tools/isochrone-tool/IsochroneTool.test.ts index bfbae26e..d9cc5503 100644 --- a/test/tools/isochrone-tool/IsochroneTool.test.ts +++ b/test/tools/isochrone-tool/IsochroneTool.test.ts @@ -179,7 +179,7 @@ describe('IsochroneTool', () => { generalize: 1000 }, // eslint-disable-next-line @typescript-eslint/no-explicit-any - { authInfo: { token } } as any + { http: { authInfo: { token } } } as any ); expect(result.isError).toBe(false); diff --git a/test/tools/lenientOutputSchema.test.ts b/test/tools/lenientOutputSchema.test.ts new file mode 100644 index 00000000..dd676abc --- /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 run() 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 }); + } + + async run(): 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(); + }); +}); diff --git a/test/tools/map-matching-tool/MapMatchingTool.test.ts b/test/tools/map-matching-tool/MapMatchingTool.test.ts index c77e7203..35baff12 100644 --- a/test/tools/map-matching-tool/MapMatchingTool.test.ts +++ b/test/tools/map-matching-tool/MapMatchingTool.test.ts @@ -343,7 +343,7 @@ describe('MapMatchingTool', () => { profile: 'driving' }, // eslint-disable-next-line @typescript-eslint/no-explicit-any - { authInfo: { token } } as any + { http: { authInfo: { token } } } as any ); expect(result.isError).toBe(false); diff --git a/test/tools/optimization-tool/OptimizationTool.test.ts b/test/tools/optimization-tool/OptimizationTool.test.ts index 42530752..60d46823 100644 --- a/test/tools/optimization-tool/OptimizationTool.test.ts +++ b/test/tools/optimization-tool/OptimizationTool.test.ts @@ -291,7 +291,7 @@ describe('OptimizationTool V1 API', () => { ] }, // eslint-disable-next-line @typescript-eslint/no-explicit-any - { authInfo: { token } } as any + { http: { authInfo: { token } } } as any ); expect(result.isError).toBe(false); diff --git a/test/tools/render-map-tool/RenderMapTool.test.ts b/test/tools/render-map-tool/RenderMapTool.test.ts index db28643f..4715ee10 100644 --- a/test/tools/render-map-tool/RenderMapTool.test.ts +++ b/test/tools/render-map-tool/RenderMapTool.test.ts @@ -64,7 +64,7 @@ describe('RenderMapTool', () => { ] }, // eslint-disable-next-line @typescript-eslint/no-explicit-any - { authInfo: { token } } as any + { http: { authInfo: { token } } } as any ); expect(result.isError).toBe(false); @@ -116,7 +116,7 @@ describe('RenderMapTool', () => { markers: [{ coordinates: [-77, 38], style: 'start' }] }, // eslint-disable-next-line @typescript-eslint/no-explicit-any - { authInfo: { token } } as any + { http: { authInfo: { token } } } as any ); expect(result.isError).toBe(false); @@ -162,7 +162,7 @@ describe('RenderMapTool', () => { ] }, // eslint-disable-next-line @typescript-eslint/no-explicit-any - { authInfo: { token } } as any + { http: { authInfo: { token } } } as any ); expect(result.isError).toBe(false); @@ -230,7 +230,7 @@ describe('RenderMapTool', () => { const result = await tool.run( { payload_refs: [ref] }, // eslint-disable-next-line @typescript-eslint/no-explicit-any - { authInfo: { token } } as any + { http: { authInfo: { token } } } as any ); expect(result.isError).toBe(false); const sc = result.structuredContent as { @@ -308,7 +308,7 @@ describe('RenderMapTool', () => { const result = await tool.run( { payload_refs: [a, b] }, // eslint-disable-next-line @typescript-eslint/no-explicit-any - { authInfo: { token } } as any + { http: { authInfo: { token } } } as any ); expect(result.isError).toBe(false); const sc = result.structuredContent as { @@ -323,8 +323,10 @@ describe('RenderMapTool', () => { const tool = new RenderMapTool({ httpRequest: vi.fn() }); const result = await tool.run( { payload_refs: ['mapbox://temp/map-payload-does-not-exist'] }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - { authInfo: { token: tokenFor('account-test-render-map-stale') } } as any + + { + http: { authInfo: { token: tokenFor('account-test-render-map-stale') } } + } as any ); expect(result.isError).toBe(true); const text = ( @@ -368,7 +370,7 @@ describe('RenderMapTool', () => { const result = await tool.run( { payload_refs: [goodRef, 'mapbox://temp/map-payload-stale-one'] }, // eslint-disable-next-line @typescript-eslint/no-explicit-any - { authInfo: { token } } as any + { http: { authInfo: { token } } } as any ); expect(result.isError).toBe(false); const sc = result.structuredContent as { layer_count: number }; @@ -395,7 +397,7 @@ describe('RenderMapTool', () => { const result = await tool.run( { payload_refs: [badRef], summary: '15-min drive-time area' }, // eslint-disable-next-line @typescript-eslint/no-explicit-any - { authInfo: { token: tokenFor('alice') } } as any + { http: { authInfo: { token: tokenFor('alice') } } } as any ); expect(result.isError).toBe(true); const text = (result.content[0] as { type: 'text'; text: string }).text; diff --git a/test/tools/search-and-geocode-tool/SearchAndGeocodeTool.test.ts b/test/tools/search-and-geocode-tool/SearchAndGeocodeTool.test.ts index 5386c6a1..8702b28b 100644 --- a/test/tools/search-and-geocode-tool/SearchAndGeocodeTool.test.ts +++ b/test/tools/search-and-geocode-tool/SearchAndGeocodeTool.test.ts @@ -699,7 +699,7 @@ describe('SearchAndGeocodeTool', () => { proximity: { longitude: -122.4194, latitude: 37.7749 } }, // eslint-disable-next-line @typescript-eslint/no-explicit-any - { authInfo: { token } } as any + { http: { authInfo: { token } } } as any ); expect(result.isError).toBe(false); diff --git a/test/tools/union-tool/UnionTool.test.ts b/test/tools/union-tool/UnionTool.test.ts index 076038a1..10339619 100644 --- a/test/tools/union-tool/UnionTool.test.ts +++ b/test/tools/union-tool/UnionTool.test.ts @@ -189,7 +189,7 @@ describe('UnionTool', () => { ] }, // eslint-disable-next-line @typescript-eslint/no-explicit-any - { authInfo: { token } } as any + { http: { authInfo: { token } } } as any ); expect(result.isError).toBe(false); diff --git a/test/utils/jsonSchema2020.test.ts b/test/utils/jsonSchema2020.test.ts index d88e9d15..93ca8a84 100644 --- a/test/utils/jsonSchema2020.test.ts +++ b/test/utils/jsonSchema2020.test.ts @@ -2,12 +2,10 @@ // Licensed under the MIT License. import { describe, it, expect } 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 { Client } from '@modelcontextprotocol/client'; +import { McpServer, InMemoryTransport } from '@modelcontextprotocol/server'; import { JSON_SCHEMA_2020_12, - publishJsonSchema2020, toJsonSchema2020 } from '../../src/utils/jsonSchema2020.js'; import { getAllTools } from '../../src/tools/toolRegistry.js'; @@ -138,10 +136,9 @@ describe('toJsonSchema2020', () => { }); }); -describe('publishJsonSchema2020', () => { - async function listTools(publish: boolean) { +describe('tools/list schemas', () => { + async function listTools() { const server = new McpServer({ name: 'test', version: '0.0.0' }); - if (publish) publishJsonSchema2020(server); for (const tool of getAllTools()) tool.installTo(server); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); @@ -157,9 +154,10 @@ describe('publishJsonSchema2020', () => { // Regression: Claude Desktop rejected every tool call with "Tool // 'search_and_geocode_tool' has an invalid outputSchema: JSON Schema - // declares an unsupported dialect" because the SDK emits draft-07. + // declares an unsupported dialect" because SDK v1 emitted draft-07. SDK v2 + // publishes 2020-12 itself; this guards against that regressing. it('publishes every tool schema as JSON Schema 2020-12', async () => { - const tools = await listTools(true); + const tools = await listTools(); expect(tools.length).toBe(getAllTools().length); for (const tool of tools) { expect(tool.inputSchema.$schema, tool.name).toBe(JSON_SCHEMA_2020_12); @@ -171,19 +169,8 @@ describe('publishJsonSchema2020', () => { } }); - it('is needed: the SDK alone publishes draft-07 with tuple items', async () => { - const tools = await listTools(false); - const leftovers = tools.flatMap((t) => [ - ...draft07Leftovers(t.inputSchema), - ...draft07Leftovers(t.outputSchema) - ]); - expect(leftovers.some((l) => l.endsWith('.$schema'))).toBe(true); - expect(leftovers.some((l) => l.endsWith('.items[]'))).toBe(true); - }); - it('keeps tool calls working with structured output validation', async () => { const server = new McpServer({ name: 'test', version: '0.0.0' }); - publishJsonSchema2020(server); const bbox = getAllTools().find((t) => t.name === 'bbox_tool')!; bbox.installTo(server); const [clientTransport, serverTransport] = From 7212f80cc25494d2fa59c9b4cd7dc96c26c41a4d Mon Sep 17 00:00:00 2001 From: Matthew Podwysocki Date: Wed, 21 Oct 2026 15:19:32 -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 19f28c00..b2968bb8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ### Breaking Changes -- **MCP TypeScript SDK v2 (#245).** The server and the `@mapbox/mcp-server/tools`, `/resources` and `/utils` 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 the line that gets new MCP features, such as the SDK's Skills extension support, and ext-apps 2.x requires it. +- **MCP TypeScript SDK v2 (#296, part of #245).** The server and the `@mapbox/mcp-server/tools`, `/resources` and `/utils` 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 the line that gets new MCP features, such as the SDK's Skills extension support, and ext-apps 2.x requires it. - **For code that imports our tools:** `installTo(server)` needs a v2 `McpServer`. A v1 server from `@modelcontextprotocol/sdk` won't work, so a deployment that builds its own server (the hosted server, for one) has to move to v2 in the same release. Tool and resource handlers now receive the v2 context: the access token is at `ctx.http.authInfo.token` instead of `extra.authInfo.token`. - **Clients:** nothing changes. The server still answers the `initialize` handshake and negotiates 2025-11-25 with current clients, and the MCP Apps wire protocol is the same in ext-apps 2.x, so `render_map_tool` renders as before. Adopting the 2026-07-28 protocol revision (MRTR in place of elicitation, the Sampling replacement) is separate work under #245. - **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` still publishes the real JSON Schema, and validation only warns. A tool with an output schema that returns only text gets `structuredContent: {}` instead of an error, since v2 checks for it before validating. `patches/` is now empty. From f0bed36d56e506210d667cad7c1aefce8cf7aaa4 Mon Sep 17 00:00:00 2001 From: Matthew Podwysocki Date: Wed, 21 Oct 2026 19:14:45 -0400 Subject: [PATCH 3/4] test: fix all test type errors and typecheck tests in CI Follow-up to #296. Adds structuredOf/textOf/resourceTextOf test helpers, fixes the ~450 errors tsconfig.test.json reported (about 200 already on main, the rest from SDK v2's open structuredContent type), adds noEmit to tsconfig.test.json and runs `npm run typecheck` in CI. Fixes tests the errors exposed: the zod 4 tuple traversal that never looked inside arrays, a structured-content test importing a nonexistent file, a stale directions input shape, and a SimplifyTool comparison that couldn't fail. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/test.yml | 3 + CHANGELOG.md | 6 + package.json | 1 + test/prompts/BasePrompt.test.ts | 35 ++++-- .../CategoryListResource.test.ts | 5 +- .../resources/compute/ComputeResource.test.ts | 7 +- .../InlinePayloadResource.test.ts | 5 +- .../temporary/TemporaryDataResource.test.ts | 39 +++--- .../ui-apps/MapAppUIResource.test.ts | 16 +-- .../ui-apps/mapAppHtml.script.test.ts | 14 +-- test/tools/area-tool/AreaTool.test.ts | 84 ++++++++----- test/tools/bearing-tool/BearingTool.test.ts | 72 +++++++---- .../bounding-box-tool/BoundingBoxTool.test.ts | 65 ++++++---- test/tools/buffer-tool/BufferTool.test.ts | 96 ++++++++------ test/tools/centroid-tool/CentroidTool.test.ts | 92 ++++++++------ test/tools/convex-tool/ConvexTool.test.ts | 20 +-- .../destination-tool/DestinationTool.test.ts | 34 +++-- .../difference-tool/DifferenceTool.test.ts | 62 +++++---- .../directions-tool/DirectionsTool.test.ts | 5 +- .../directions-tool/cleanResponseData.test.ts | 83 +++++++----- test/tools/distance-tool/DistanceTool.test.ts | 36 ++++-- test/tools/input-schema-validation.test.ts | 32 ++--- .../intersect-tool/IntersectTool.test.ts | 60 +++++---- test/tools/length-tool/LengthTool.test.ts | 24 ++-- test/tools/midpoint-tool/MidpointTool.test.ts | 100 ++++++++------- .../NearestPointOnLineTool.test.ts | 42 +++++-- .../NearestPointTool.test.ts | 27 ++-- .../OptimizationTool.test.ts | 17 ++- .../PointsWithinPolygonTool.test.ts | 36 +++--- test/tools/simplify-tool/SimplifyTool.test.ts | 119 +++++++++--------- test/tools/structured-content.test.ts | 13 +- test/tools/union-tool/UnionTool.test.ts | 56 ++++----- test/utils/directionsRef.test.ts | 4 +- test/utils/mapboxPublicToken.test.ts | 13 +- test/utils/resultTestUtils.ts | 41 ++++++ tsconfig.test.json | 5 +- 36 files changed, 811 insertions(+), 558 deletions(-) create mode 100644 test/utils/resultTestUtils.ts diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ab3970ff..35ebffc3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,5 +26,8 @@ jobs: - name: Compile typescript run: npm run build + - name: Typecheck src and tests + run: npm run typecheck + - name: Run tests run: npm test \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index b2968bb8..a77088d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,12 @@ - **Token scope:** it needs `user-feedback:write` to send, and `user-feedback:read` for the duplicate check. On a 401 or 403 when sending, the error says so. Without the read scope, or if the list call fails, the report is sent unchecked and the result says why. `--disable-tools feedback_tool` leaves it out. - Annotated `readOnlyHint: false`, `destructiveHint: false`, `idempotentHint: false`. The location goes as top-level `lat`/`lon`; the optional GeoJSON `feature` and `screenshot` fields aren't sent yet, since the docs give `feature` coordinates as `[latitude, longitude]` and that needs checking first. +### Maintenance + +- **Tests are typechecked in CI (#PR).** `tsconfig.test.json` covers `src/` and `test/`, but nothing ran it, so test type errors piled up: about 200 on main, plus about 250 more from SDK v2 typing `structuredContent` as an open object (#296). They're all fixed, and `npm run typecheck` now runs in CI before the tests. `tsconfig.test.json` also gets `noEmit`, since running it without that writes `.js` files next to the sources (see #297). + - **New helpers** in `test/utils/resultTestUtils.ts`: `structuredOf(result)` reads structured content as the tool's own output type, so a test that checks a field the schema doesn't have fails to compile. `textOf` and `resourceTextOf` read text content and throw on the wrong content type. + - **Tests that were wrong:** the tuple check in `input-schema-validation.test.ts` never looked inside arrays under zod 4 (`ZodArray._def.type` is now the string `'array'`), so a tuple nested in an array would have passed. It uses zod 4's accessors now and covers that case. `structured-content.test.ts` imported a file that never existed. `cleanResponseData.test.ts` used an old directions input shape and now builds it with `DirectionsInputSchema.parse`. A `|| Infinity` fallback in a SimplifyTool test meant one comparison could never fail. + ## 0.16.1-dev - 2026-10-16 ## 0.16.0 - 2026-10-15 diff --git a/package.json b/package.json index 7d953a1d..92278d11 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "inspect:build": "npm run build && npx @modelcontextprotocol/inspector -e MAPBOX_ACCESS_TOKEN=\"$MAPBOX_ACCESS_TOKEN\" node dist/esm/index.js", "inspect:dev": "npx @modelcontextprotocol/inspector -e MAPBOX_ACCESS_TOKEN=\"$MAPBOX_ACCESS_TOKEN\" npx -y tsx src/index.ts", "dev:http": "npx -y tsx scripts/dev-http-server.ts", + "typecheck": "tsc -p tsconfig.test.json", "lint": "eslint \"./src/**/*.{ts,tsx}\" \"./test/**/*.{ts,tsx}\" \"./examples/**/*.{ts,tsx}\"", "lint:fix": "eslint \"./src/**/*.{ts,tsx}\" \"./test/**/*.{ts,tsx}\" \"./examples/**/*.{ts,tsx}\" --fix", "postinstall": "patch-package || (cd ../../.. && node ./node_modules/patch-package/index.js --patch-dir ./node_modules/@mapbox/mcp-server/patches) || true", diff --git a/test/prompts/BasePrompt.test.ts b/test/prompts/BasePrompt.test.ts index 67d3869c..bd791ce2 100644 --- a/test/prompts/BasePrompt.test.ts +++ b/test/prompts/BasePrompt.test.ts @@ -8,8 +8,23 @@ import type { PromptMessage } from '@modelcontextprotocol/server'; +// Makes validateArguments public so tests can call it directly. +abstract class TestableBasePrompt extends BasePrompt { + override validateArguments(args: Record): void { + super.validateArguments(args); + } +} + +// The text of a prompt message. Throws if the content isn't text. +function messageText(message: PromptMessage): string { + if (message.content.type !== 'text') { + throw new Error(`message content is ${message.content.type}, not text`); + } + return message.content.text; +} + // Create a concrete test implementation of BasePrompt -class TestPrompt extends BasePrompt { +class TestPrompt extends TestableBasePrompt { readonly name = 'test-prompt'; readonly description = 'A test prompt for unit testing'; readonly arguments: PromptArgument[] = [ @@ -173,7 +188,7 @@ describe('BasePrompt', () => { expect(message.role).toBeDefined(); expect(message.content).toBeDefined(); expect(message.content.type).toBe('text'); - expect(typeof message.content.text).toBe('string'); + expect(typeof messageText(message)).toBe('string'); }); }); @@ -194,10 +209,10 @@ describe('BasePrompt', () => { }; const messages = prompt.getMessages(args); - const messageText = messages[0].content.text; + const text = messageText(messages[0]); - expect(messageText).toContain('test_required'); - expect(messageText).toContain('test_optional'); + expect(text).toContain('test_required'); + expect(text).toContain('test_optional'); }); test('handles missing optional arguments gracefully', () => { @@ -206,16 +221,16 @@ describe('BasePrompt', () => { }; const messages = prompt.getMessages(args); - const messageText = messages[0].content.text; + const text = messageText(messages[0]); - expect(messageText).toContain('test_required'); - expect(messageText).toContain('not provided'); + expect(text).toContain('test_required'); + expect(text).toContain('not provided'); }); }); describe('edge cases', () => { test('validates empty args object when no arguments are required', () => { - class NoArgsPrompt extends BasePrompt { + class NoArgsPrompt extends TestableBasePrompt { readonly name = 'no-args'; readonly description = 'No arguments needed'; readonly arguments: PromptArgument[] = []; @@ -239,7 +254,7 @@ describe('BasePrompt', () => { }); test('validates all required arguments', () => { - class MultiRequiredPrompt extends BasePrompt { + class MultiRequiredPrompt extends TestableBasePrompt { readonly name = 'multi-required'; readonly description = 'Multiple required arguments'; readonly arguments: PromptArgument[] = [ diff --git a/test/resources/category-list/CategoryListResource.test.ts b/test/resources/category-list/CategoryListResource.test.ts index 2d0f450e..31dda987 100644 --- a/test/resources/category-list/CategoryListResource.test.ts +++ b/test/resources/category-list/CategoryListResource.test.ts @@ -9,6 +9,7 @@ import { assertHeadersSent } from '../../utils/httpPipelineUtils.js'; import { CategoryListResource } from '../../../src/resources/category-list/CategoryListResource.js'; +import { resourceTextOf } from '../../utils/resultTestUtils.js'; describe('CategoryListResource', () => { afterEach(() => { @@ -111,7 +112,7 @@ describe('CategoryListResource', () => { expect(result.contents[0].uri).toBe('mapbox://categories'); expect(result.contents[0].mimeType).toBe('application/json'); - const parsed = JSON.parse(result.contents[0].text!); + const parsed = JSON.parse(resourceTextOf(result)); expect(parsed.listItems).toEqual(['restaurant', 'hotel']); expect(parsed.version).toBe('25:test'); expect(parsed.attribution).toBe('Test Attribution'); @@ -131,7 +132,7 @@ describe('CategoryListResource', () => { ); expect(result.contents).toHaveLength(1); - const parsed = JSON.parse(result.contents[0].text!); + const parsed = JSON.parse(resourceTextOf(result)); expect(parsed.listItems).toEqual([]); }); diff --git a/test/resources/compute/ComputeResource.test.ts b/test/resources/compute/ComputeResource.test.ts index 25af05cf..43a2cee5 100644 --- a/test/resources/compute/ComputeResource.test.ts +++ b/test/resources/compute/ComputeResource.test.ts @@ -4,6 +4,7 @@ import { describe, it, expect } from 'vitest'; import { ComputeResource } from '../../../src/resources/compute/ComputeResource.js'; import { buildComputeRef } from '../../../src/utils/computeRef.js'; +import { resourceTextOf } from '../../utils/resultTestUtils.js'; describe('ComputeResource', () => { it('resolves a union compute ref via the real resources/read path with zero prior server-side state', async () => { @@ -47,7 +48,7 @@ describe('ComputeResource', () => { const result = await resource.read(ref); expect(result.contents[0].mimeType).toBe('application/json'); - const payload = JSON.parse(result.contents[0].text as string); + const payload = JSON.parse(resourceTextOf(result)); expect(payload.layers).toHaveLength(6); expect(payload.summary).toBe('Union of 2 polygons'); }); @@ -90,7 +91,7 @@ describe('ComputeResource', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const asAccountB = { http: { authInfo: { token: 'irrelevant' } } } as any; const result = await resource.read(ref, asAccountB); - const payload = JSON.parse(result.contents[0].text as string); + const payload = JSON.parse(resourceTextOf(result)); expect(payload.summary).toBe('Intersection of two polygons'); }); @@ -99,6 +100,6 @@ describe('ComputeResource', () => { const result = await resource.read('mapbox://compute/union?data=!!!'); expect(result.contents[0].mimeType).toBe('text/plain'); - expect(result.contents[0].text).toContain('malformed or unsupported'); + expect(resourceTextOf(result)).toContain('malformed or unsupported'); }); }); diff --git a/test/resources/inline-payload/InlinePayloadResource.test.ts b/test/resources/inline-payload/InlinePayloadResource.test.ts index 091d075d..ffda1409 100644 --- a/test/resources/inline-payload/InlinePayloadResource.test.ts +++ b/test/resources/inline-payload/InlinePayloadResource.test.ts @@ -4,6 +4,7 @@ import { describe, it, expect } from 'vitest'; import { InlinePayloadResource } from '../../../src/resources/inline-payload/InlinePayloadResource.js'; import { buildInlinePayloadRef } from '../../../src/utils/inlinePayloadRef.js'; +import { resourceTextOf } from '../../utils/resultTestUtils.js'; describe('InlinePayloadResource', () => { it("resolves render_map_tool's self-describing merged ref via the real resources/read path with zero prior server-side state", async () => { @@ -33,7 +34,7 @@ describe('InlinePayloadResource', () => { const result = await resource.read(ref); expect(result.contents[0].mimeType).toBe('application/json'); - const payload = JSON.parse(result.contents[0].text as string); + const payload = JSON.parse(resourceTextOf(result)); expect(payload.summary).toBe('Test trip'); expect(payload.layers).toHaveLength(1); expect(payload.markers).toHaveLength(1); @@ -44,6 +45,6 @@ describe('InlinePayloadResource', () => { const result = await resource.read('mapbox://inline/payload?data=!!!'); expect(result.contents[0].mimeType).toBe('text/plain'); - expect(result.contents[0].text).toContain('malformed'); + expect(resourceTextOf(result)).toContain('malformed'); }); }); diff --git a/test/resources/temporary/TemporaryDataResource.test.ts b/test/resources/temporary/TemporaryDataResource.test.ts index 97dfb4b3..730d3c09 100644 --- a/test/resources/temporary/TemporaryDataResource.test.ts +++ b/test/resources/temporary/TemporaryDataResource.test.ts @@ -12,6 +12,15 @@ import { getOwnerKeyFromToken, getUserNameFromToken } from '../../../src/utils/jwtUtils.js'; +import type { ReadResourceResult } from '@modelcontextprotocol/server'; +import { resourceTextOf } from '../../utils/resultTestUtils.js'; + +// The blob of a resource read's first contents entry, or undefined when +// it's text. +function blobOf(result: ReadResourceResult): string | undefined { + const entry = result.contents[0]; + return 'blob' in entry ? entry.blob : undefined; +} // Build a Mapbox-style 3-part JWT whose payload carries the username (`u`). function tokenFor(username: string): string { @@ -82,7 +91,7 @@ describe('TemporaryDataResource — AGI-890 cross-account access control', () => seedTextResource(uri, 'accountA', { route: 'A-secret-geometry' }); const result = await resource.read(uri, extraFor(tokenFor('accountA'))); - const text = result.contents[0].text as string; + const text = resourceTextOf(result); expect(text).toContain('A-secret-geometry'); }); @@ -92,7 +101,7 @@ describe('TemporaryDataResource — AGI-890 cross-account access control', () => seedTextResource(uri, 'accountA', { route: 'A-secret-geometry' }); const result = await resource.read(uri, extraFor(tokenFor('accountB'))); - const text = result.contents[0].text as string; + const text = resourceTextOf(result); expect(text).toBe(NOT_FOUND); expect(text).not.toContain('A-secret-geometry'); @@ -117,8 +126,8 @@ describe('TemporaryDataResource — AGI-890 cross-account access control', () => expect(crossAccount.contents[0].mimeType).toBe( missing.contents[0].mimeType ); - expect(crossAccount.contents[0].text).toBe(missing.contents[0].text); - expect(crossAccount.contents[0].text).toBe(NOT_FOUND); + expect(resourceTextOf(crossAccount)).toBe(resourceTextOf(missing)); + expect(resourceTextOf(crossAccount)).toBe(NOT_FOUND); }); it('fails closed when the reader has no token', async () => { @@ -126,7 +135,7 @@ describe('TemporaryDataResource — AGI-890 cross-account access control', () => seedTextResource(uri, 'accountA', { route: 'A-secret-geometry' }); const result = await resource.read(uri, extraFor(undefined)); - expect(result.contents[0].text).toBe(NOT_FOUND); + expect(resourceTextOf(result)).toBe(NOT_FOUND); }); it('fails closed when the resource has no owner recorded', async () => { @@ -139,7 +148,7 @@ describe('TemporaryDataResource — AGI-890 cross-account access control', () => }); const result = await resource.read(uri, extraFor(tokenFor('accountA'))); - expect(result.contents[0].text).toBe(NOT_FOUND); + expect(resourceTextOf(result)).toBe(NOT_FOUND); }); it('falls back to the env token so stdio/single-user reads still work', async () => { @@ -150,7 +159,7 @@ describe('TemporaryDataResource — AGI-890 cross-account access control', () => // No authInfo on the request (stdio) -> requester resolved from env token. const result = await resource.read(uri, extraFor(undefined)); - expect(result.contents[0].text as string).toContain('local-data'); + expect(resourceTextOf(result)).toContain('local-data'); }); it('returns image blobs to the owner and not-found to others', async () => { @@ -165,12 +174,12 @@ describe('TemporaryDataResource — AGI-890 cross-account access control', () => }); const owner = await resource.read(uri, extraFor(tokenFor('accountA'))); - expect(owner.contents[0].blob).toBe('BASE64IMAGEDATA'); + expect(blobOf(owner)).toBe('BASE64IMAGEDATA'); expect(owner.contents[0].mimeType).toBe('image/png'); const other = await resource.read(uri, extraFor(tokenFor('accountB'))); - expect(other.contents[0].blob).toBeUndefined(); - expect(other.contents[0].text).toBe(NOT_FOUND); + expect(blobOf(other)).toBeUndefined(); + expect(resourceTextOf(other)).toBe(NOT_FOUND); }); it('resolves a storeMapPayload ref via the real resources/read path (regression)', async () => { @@ -185,7 +194,7 @@ describe('TemporaryDataResource — AGI-890 cross-account access control', () => ); const result = await resource.read(ref, extraFor(tokenFor('accountA'))); - const parsed = JSON.parse(result.contents[0].text as string); + const parsed = JSON.parse(resourceTextOf(result)); expect(parsed.summary).toBe('Test route'); }); @@ -210,8 +219,8 @@ describe('TemporaryDataResource — AGI-890 cross-account access control', () => const result = await resource.read(uri, extraFor(forged)); - expect(result.contents[0].text).toBe(NOT_FOUND); - expect(result.contents[0].text).not.toContain('A-secret-geometry'); + expect(resourceTextOf(result)).toBe(NOT_FOUND); + expect(resourceTextOf(result)).not.toContain('A-secret-geometry'); }); it('does NOT return an image blob to a token forging the owner’s username', async () => { @@ -230,8 +239,8 @@ describe('TemporaryDataResource — AGI-890 cross-account access control', () => extraFor(forgedTokenFor('victim_user')) ); - expect(result.contents[0].blob).toBeUndefined(); - expect(result.contents[0].text).toBe(NOT_FOUND); + expect(blobOf(result)).toBeUndefined(); + expect(resourceTextOf(result)).toBe(NOT_FOUND); }); it('does NOT resolve a map-payload ref for a token forging the owner’s username', async () => { diff --git a/test/resources/ui-apps/MapAppUIResource.test.ts b/test/resources/ui-apps/MapAppUIResource.test.ts index 01eedcdc..c5ff098c 100644 --- a/test/resources/ui-apps/MapAppUIResource.test.ts +++ b/test/resources/ui-apps/MapAppUIResource.test.ts @@ -7,6 +7,8 @@ const SK_TOKEN = 'sk.eyJzdWIiOiJ0ZXN0IiwidSI6InRlc3R1c2VyIn0.signature'; import { describe, it, expect, afterEach, beforeEach, vi } from 'vitest'; import { MapAppUIResource } from '../../../src/resources/ui-apps/MapAppUIResource.js'; import { __resetMapboxPublicTokenCache } from '../../../src/utils/mapboxPublicToken.js'; +import { resourceTextOf } from '../../utils/resultTestUtils.js'; +import type { HttpRequest } from '../../../src/utils/types.js'; const fakeTokenList = [ { @@ -39,7 +41,8 @@ describe('MapAppUIResource', () => { }); it('serves HTML at ui://mapbox/map-app/index.html with the mcp-app mime type', async () => { - const httpRequest = vi.fn(async (url: string) => { + const httpRequest = vi.fn(async (input) => { + const url = String(input); if (url.includes('tokens/v2/')) return makeOkJson(fakeTokenList) as Response; throw new Error(`Unexpected URL: ${url}`); @@ -57,11 +60,10 @@ describe('MapAppUIResource', () => { const entry = result.contents[0]; expect(entry.mimeType).toBe('text/html;profile=mcp-app'); expect(entry.uri).toBe('ui://mapbox/map-app/index.html'); - expect(typeof entry.text).toBe('string'); - expect(entry.text as string).toContain( - 'pk.eyJ1IjoidGVzdHVzZXIifQ.fake-public-token' - ); - expect(entry.text as string).toContain('mapbox-gl.js'); + const text = resourceTextOf(result); + expect(typeof text).toBe('string'); + expect(text).toContain('pk.eyJ1IjoidGVzdHVzZXIifQ.fake-public-token'); + expect(text).toContain('mapbox-gl.js'); const meta = (entry as { _meta?: unknown })._meta as | { ui?: { csp?: Record } } @@ -86,7 +88,7 @@ describe('MapAppUIResource', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any); - expect(result.contents[0].text as string).toContain( + expect(resourceTextOf(result)).toContain( 'No Mapbox public token available' ); }); diff --git a/test/resources/ui-apps/mapAppHtml.script.test.ts b/test/resources/ui-apps/mapAppHtml.script.test.ts index b7b72ee0..90c42733 100644 --- a/test/resources/ui-apps/mapAppHtml.script.test.ts +++ b/test/resources/ui-apps/mapAppHtml.script.test.ts @@ -681,7 +681,7 @@ describe('mapAppHtml directions self-fetch', () => { map.addLayer = addLayerSpy; map.addSource = addSourceSpy; - const fetchSpy = vi.fn(async () => ({ + const fetchSpy = vi.fn(async (_url: string) => ({ ok: true, json: async () => ({ routes: [ @@ -938,7 +938,7 @@ describe('mapAppHtml isochrone self-fetch', () => { map.addLayer = addLayerSpy; map.addSource = addSourceSpy; - const fetchSpy = vi.fn(async () => ({ + const fetchSpy = vi.fn(async (_url: string) => ({ ok: true, json: async () => ({ type: 'FeatureCollection', @@ -1044,7 +1044,7 @@ describe('mapAppHtml map matching self-fetch', () => { map.addLayer = addLayerSpy; map.addSource = addSourceSpy; - const fetchSpy = vi.fn(async () => ({ + const fetchSpy = vi.fn(async (_url: string) => ({ ok: true, json: async () => ({ code: 'Ok', @@ -1187,7 +1187,7 @@ describe('mapAppHtml search self-fetch', () => { map.addLayer = addLayerSpy; map.addSource = addSourceSpy; - const fetchSpy = vi.fn(async () => ({ + const fetchSpy = vi.fn(async (_url: string) => ({ ok: true, json: async () => ({ type: 'FeatureCollection', @@ -1320,7 +1320,7 @@ describe('mapAppHtml category search self-fetch', () => { map.addLayer = addLayerSpy; map.addSource = addSourceSpy; - const fetchSpy = vi.fn(async () => ({ + const fetchSpy = vi.fn(async (_url: string) => ({ ok: true, json: async () => ({ type: 'FeatureCollection', @@ -1405,7 +1405,7 @@ describe('mapAppHtml optimization self-fetch', () => { map.addLayer = addLayerSpy; map.addSource = addSourceSpy; - const fetchSpy = vi.fn(async () => ({ + const fetchSpy = vi.fn(async (_url: string) => ({ ok: true, json: async () => ({ code: 'Ok', @@ -2339,7 +2339,7 @@ describe('mapAppHtml results side panel: self-fetch (category_search)', () => { describe('mapAppHtml results side panel: inline markers', () => { it('renders and enriches the panel from inline markers that carry an id, with no selfFetch entries at all', async () => { const { sendToolResult, setFetchImpl, sidePanelEl } = loadScriptSandbox(); - const fetchSpy = vi.fn(async () => ({ + const fetchSpy = vi.fn(async (_url: string) => ({ ok: true, json: async () => ({ results: [] }) })); diff --git a/test/tools/area-tool/AreaTool.test.ts b/test/tools/area-tool/AreaTool.test.ts index a78e0c22..5e349b05 100644 --- a/test/tools/area-tool/AreaTool.test.ts +++ b/test/tools/area-tool/AreaTool.test.ts @@ -3,6 +3,8 @@ import { describe, it, expect } from 'vitest'; import { AreaTool } from '../../../src/tools/area-tool/AreaTool.js'; +import type { AreaOutput } from '../../../src/tools/area-tool/AreaTool.output.schema.js'; +import { structuredOf } from '../../utils/resultTestUtils.js'; describe('AreaTool', () => { const tool = new AreaTool(); @@ -20,12 +22,13 @@ describe('AreaTool', () => { ] ] }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.units).toBe('meters'); + expect(data.units).toBe('meters'); // Should be approximately 1,237,000 square meters (varies by latitude) - expect(result.structuredContent?.area).toBeGreaterThan(1000000); - expect(result.structuredContent?.area).toBeLessThan(1500000); + expect(data.area).toBeGreaterThan(1000000); + expect(data.area).toBeLessThan(1500000); }); it('should calculate area in square kilometers', async () => { @@ -41,12 +44,13 @@ describe('AreaTool', () => { ], units: 'kilometers' }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.units).toBe('kilometers'); + expect(data.units).toBe('kilometers'); // Should be approximately 1.2 square kilometers - expect(result.structuredContent?.area).toBeGreaterThan(1); - expect(result.structuredContent?.area).toBeLessThan(1.5); + expect(data.area).toBeGreaterThan(1); + expect(data.area).toBeLessThan(1.5); }); it('should calculate area in square feet', async () => { @@ -62,10 +66,11 @@ describe('AreaTool', () => { ], units: 'feet' }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.units).toBe('feet'); - expect(result.structuredContent?.area).toBeGreaterThan(100000); + expect(data.units).toBe('feet'); + expect(data.area).toBeGreaterThan(100000); }); it('should calculate area in square miles', async () => { @@ -81,12 +86,13 @@ describe('AreaTool', () => { ], units: 'miles' }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.units).toBe('miles'); + expect(data.units).toBe('miles'); // Should be roughly 47 square miles - expect(result.structuredContent?.area).toBeGreaterThan(40); - expect(result.structuredContent?.area).toBeLessThan(50); + expect(data.area).toBeGreaterThan(40); + expect(data.area).toBeLessThan(50); }); it('should calculate area in acres', async () => { @@ -102,12 +108,13 @@ describe('AreaTool', () => { ], units: 'acres' }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.units).toBe('acres'); + expect(data.units).toBe('acres'); // Should be approximately 305 acres - expect(result.structuredContent?.area).toBeGreaterThan(250); - expect(result.structuredContent?.area).toBeLessThan(350); + expect(data.area).toBeGreaterThan(250); + expect(data.area).toBeLessThan(350); }); it('should calculate area in hectares', async () => { @@ -123,12 +130,13 @@ describe('AreaTool', () => { ], units: 'hectares' }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.units).toBe('hectares'); + expect(data.units).toBe('hectares'); // Should be approximately 124 hectares - expect(result.structuredContent?.area).toBeGreaterThan(100); - expect(result.structuredContent?.area).toBeLessThan(150); + expect(data.area).toBeGreaterThan(100); + expect(data.area).toBeLessThan(150); }); it('should calculate area of triangle', async () => { @@ -143,11 +151,12 @@ describe('AreaTool', () => { ], units: 'kilometers' }); + const data = structuredOf(result); expect(result.isError).toBe(false); // Triangle should have roughly half the area of the rectangle - expect(result.structuredContent?.area).toBeGreaterThan(0.5); - expect(result.structuredContent?.area).toBeLessThan(1); + expect(data.area).toBeGreaterThan(0.5); + expect(data.area).toBeLessThan(1); }); it('should calculate area of polygon with hole', async () => { @@ -172,11 +181,12 @@ describe('AreaTool', () => { ], units: 'kilometers' }); + const data = structuredOf(result); expect(result.isError).toBe(false); // Area should be outer minus inner (roughly 4 - 1 = 3 km²) - expect(result.structuredContent?.area).toBeGreaterThan(2.5); - expect(result.structuredContent?.area).toBeLessThan(4); + expect(data.area).toBeGreaterThan(2.5); + expect(data.area).toBeLessThan(4); }); it('should calculate area of multipolygon', async () => { @@ -205,11 +215,12 @@ describe('AreaTool', () => { ], units: 'kilometers' }); + const data = structuredOf(result); expect(result.isError).toBe(false); // Total area should be roughly 2.4 km² - expect(result.structuredContent?.area).toBeGreaterThan(2); - expect(result.structuredContent?.area).toBeLessThan(3); + expect(data.area).toBeGreaterThan(2); + expect(data.area).toBeLessThan(3); }); it('should calculate area of irregular polygon', async () => { @@ -227,10 +238,11 @@ describe('AreaTool', () => { ], units: 'kilometers' }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.area).toBeGreaterThan(3); - expect(result.structuredContent?.area).toBeLessThan(7); + expect(data.area).toBeGreaterThan(3); + expect(data.area).toBeLessThan(7); }); it('should round area to 3 decimal places', async () => { @@ -246,9 +258,10 @@ describe('AreaTool', () => { ], units: 'kilometers' }); + const data = structuredOf(result); expect(result.isError).toBe(false); - const area = result.structuredContent?.area as number; + const area = data.area as number; const decimalPlaces = (area.toString().split('.')[1] || '').length; expect(decimalPlaces).toBeLessThanOrEqual(3); }); @@ -266,11 +279,12 @@ describe('AreaTool', () => { ], units: 'meters' }); + const data = structuredOf(result); expect(result.isError).toBe(false); // Should be roughly 123 square meters - expect(result.structuredContent?.area).toBeGreaterThan(100); - expect(result.structuredContent?.area).toBeLessThan(200); + expect(data.area).toBeGreaterThan(100); + expect(data.area).toBeLessThan(200); }); it('should handle large polygons', async () => { @@ -287,10 +301,11 @@ describe('AreaTool', () => { ], units: 'kilometers' }); + const data = structuredOf(result); expect(result.isError).toBe(false); // Should be hundreds of thousands of square kilometers - expect(result.structuredContent?.area).toBeGreaterThan(500000); + expect(data.area).toBeGreaterThan(500000); }); it('should calculate area in southern hemisphere', async () => { @@ -306,9 +321,10 @@ describe('AreaTool', () => { ], units: 'kilometers' }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.area).toBeGreaterThan(900000); + expect(data.area).toBeGreaterThan(900000); }); it('should handle polygon near poles', async () => { @@ -324,11 +340,12 @@ describe('AreaTool', () => { ], units: 'kilometers' }); + const data = structuredOf(result); expect(result.isError).toBe(false); // Area should be much smaller near poles due to convergence of meridians - expect(result.structuredContent?.area).toBeGreaterThan(0); - expect(result.structuredContent?.area).toBeLessThan(20000); + expect(data.area).toBeGreaterThan(0); + expect(data.area).toBeLessThan(20000); }); it('should calculate zero area for degenerate polygon (line)', async () => { @@ -342,12 +359,13 @@ describe('AreaTool', () => { ], units: 'kilometers' }); + const data = structuredOf(result); // Degenerate polygon may error or return 0 area if (result.isError) { expect(result.isError).toBe(true); } else { - expect(result.structuredContent?.area).toBe(0); + expect(data.area).toBe(0); } }); }); diff --git a/test/tools/bearing-tool/BearingTool.test.ts b/test/tools/bearing-tool/BearingTool.test.ts index b4defefd..36eecca6 100644 --- a/test/tools/bearing-tool/BearingTool.test.ts +++ b/test/tools/bearing-tool/BearingTool.test.ts @@ -3,6 +3,8 @@ import { describe, it, expect } from 'vitest'; import { BearingTool } from '../../../src/tools/bearing-tool/BearingTool.js'; +import type { BearingOutput } from '../../../src/tools/bearing-tool/BearingTool.output.schema.js'; +import { structuredOf } from '../../utils/resultTestUtils.js'; describe('BearingTool', () => { const tool = new BearingTool(); @@ -12,14 +14,15 @@ describe('BearingTool', () => { from: { longitude: 0, latitude: 0 }, to: { longitude: 0, latitude: 1 } }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.bearing).toBeCloseTo(0, 1); - expect(result.structuredContent?.from).toEqual({ + expect(data.bearing).toBeCloseTo(0, 1); + expect(data.from).toEqual({ longitude: 0, latitude: 0 }); - expect(result.structuredContent?.to).toEqual({ + expect(data.to).toEqual({ longitude: 0, latitude: 1 }); @@ -30,9 +33,10 @@ describe('BearingTool', () => { from: { longitude: 0, latitude: 0 }, to: { longitude: 1, latitude: 0 } }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.bearing).toBeCloseTo(90, 1); + expect(data.bearing).toBeCloseTo(90, 1); }); it('should calculate bearing for south direction (180°)', async () => { @@ -40,9 +44,10 @@ describe('BearingTool', () => { from: { longitude: 0, latitude: 0 }, to: { longitude: 0, latitude: -1 } }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.bearing).toBeCloseTo(180, 1); + expect(data.bearing).toBeCloseTo(180, 1); }); it('should calculate bearing for west direction (270°)', async () => { @@ -50,9 +55,10 @@ describe('BearingTool', () => { from: { longitude: 0, latitude: 0 }, to: { longitude: -1, latitude: 0 } }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.bearing).toBeCloseTo(270, 1); + expect(data.bearing).toBeCloseTo(270, 1); }); it('should calculate bearing for northeast direction (45°)', async () => { @@ -60,10 +66,11 @@ describe('BearingTool', () => { from: { longitude: 0, latitude: 0 }, to: { longitude: 1, latitude: 1 } }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.bearing).toBeGreaterThan(30); - expect(result.structuredContent?.bearing).toBeLessThan(60); + expect(data.bearing).toBeGreaterThan(30); + expect(data.bearing).toBeLessThan(60); }); it('should calculate bearing for southwest direction (225°)', async () => { @@ -71,10 +78,11 @@ describe('BearingTool', () => { from: { longitude: 0, latitude: 0 }, to: { longitude: -1, latitude: -1 } }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.bearing).toBeGreaterThan(210); - expect(result.structuredContent?.bearing).toBeLessThan(240); + expect(data.bearing).toBeGreaterThan(210); + expect(data.bearing).toBeLessThan(240); }); it('should calculate bearing between real cities (SF to LA)', async () => { @@ -82,11 +90,12 @@ describe('BearingTool', () => { from: { longitude: -122.4194, latitude: 37.7749 }, // San Francisco to: { longitude: -118.2437, latitude: 34.0522 } // Los Angeles }); + const data = structuredOf(result); expect(result.isError).toBe(false); // SF to LA should be roughly southeast - expect(result.structuredContent?.bearing).toBeGreaterThan(100); - expect(result.structuredContent?.bearing).toBeLessThan(150); + expect(data.bearing).toBeGreaterThan(100); + expect(data.bearing).toBeLessThan(150); }); it('should calculate bearing between real cities (NY to London)', async () => { @@ -94,11 +103,12 @@ describe('BearingTool', () => { from: { longitude: -74.006, latitude: 40.7128 }, // New York to: { longitude: -0.1276, latitude: 51.5074 } // London }); + const data = structuredOf(result); expect(result.isError).toBe(false); // NY to London should be roughly northeast - expect(result.structuredContent?.bearing).toBeGreaterThan(40); - expect(result.structuredContent?.bearing).toBeLessThan(90); + expect(data.bearing).toBeGreaterThan(40); + expect(data.bearing).toBeLessThan(90); }); it('should handle same point as zero bearing', async () => { @@ -106,9 +116,10 @@ describe('BearingTool', () => { from: { longitude: 0, latitude: 0 }, to: { longitude: 0, latitude: 0 } }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.bearing).toBe(0); + expect(data.bearing).toBe(0); }); it('should handle bearing across the antimeridian', async () => { @@ -116,11 +127,12 @@ describe('BearingTool', () => { from: { longitude: 179, latitude: 0 }, to: { longitude: -179, latitude: 0 } }); + const data = structuredOf(result); expect(result.isError).toBe(false); // Should be roughly east (90°) - expect(result.structuredContent?.bearing).toBeGreaterThan(70); - expect(result.structuredContent?.bearing).toBeLessThan(110); + expect(data.bearing).toBeGreaterThan(70); + expect(data.bearing).toBeLessThan(110); }); it('should handle bearing near poles', async () => { @@ -128,11 +140,12 @@ describe('BearingTool', () => { from: { longitude: 0, latitude: 85 }, to: { longitude: 90, latitude: 85 } }); + const data = structuredOf(result); expect(result.isError).toBe(false); // Near poles, bearings are distorted - just check it's a valid bearing - expect(result.structuredContent?.bearing).toBeGreaterThanOrEqual(0); - expect(result.structuredContent?.bearing).toBeLessThan(360); + expect(data.bearing).toBeGreaterThanOrEqual(0); + expect(data.bearing).toBeLessThan(360); }); it('should round bearing to 2 decimal places', async () => { @@ -140,9 +153,10 @@ describe('BearingTool', () => { from: { longitude: 0, latitude: 0 }, to: { longitude: 0.123456, latitude: 0.654321 } }); + const data = structuredOf(result); expect(result.isError).toBe(false); - const bearing = result.structuredContent?.bearing as number; + const bearing = data.bearing as number; const decimalPlaces = (bearing.toString().split('.')[1] || '').length; expect(decimalPlaces).toBeLessThanOrEqual(2); }); @@ -152,12 +166,13 @@ describe('BearingTool', () => { from: { longitude: 0, latitude: 0 }, to: { longitude: -1, latitude: 1 } }); + const data = structuredOf(result); expect(result.isError).toBe(false); // Should be northwest (between 270-360) - expect(result.structuredContent?.bearing).toBeGreaterThanOrEqual(0); - expect(result.structuredContent?.bearing).toBeLessThan(360); - expect(result.structuredContent?.bearing).toBeGreaterThan(300); + expect(data.bearing).toBeGreaterThanOrEqual(0); + expect(data.bearing).toBeLessThan(360); + expect(data.bearing).toBeGreaterThan(300); }); it('should handle negative coordinates', async () => { @@ -165,10 +180,11 @@ describe('BearingTool', () => { from: { longitude: -100, latitude: -30 }, to: { longitude: -90, latitude: -20 } }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.bearing).toBeGreaterThanOrEqual(0); - expect(result.structuredContent?.bearing).toBeLessThan(360); + expect(data.bearing).toBeGreaterThanOrEqual(0); + expect(data.bearing).toBeLessThan(360); }); it('should calculate reverse bearing correctly', async () => { @@ -176,14 +192,16 @@ describe('BearingTool', () => { from: { longitude: 0, latitude: 0 }, to: { longitude: 0, latitude: 1 } }); + const forwardResultData = structuredOf(forwardResult); const reverseResult = await tool.run({ from: { longitude: 0, latitude: 1 }, to: { longitude: 0, latitude: 0 } }); + const reverseResultData = structuredOf(reverseResult); - const forwardBearing = forwardResult.structuredContent?.bearing as number; - const reverseBearing = reverseResult.structuredContent?.bearing as number; + const forwardBearing = forwardResultData.bearing as number; + const reverseBearing = reverseResultData.bearing as number; // Reverse bearing should be ~180° different const diff = Math.abs(forwardBearing - reverseBearing); diff --git a/test/tools/bounding-box-tool/BoundingBoxTool.test.ts b/test/tools/bounding-box-tool/BoundingBoxTool.test.ts index 0033d585..357b9133 100644 --- a/test/tools/bounding-box-tool/BoundingBoxTool.test.ts +++ b/test/tools/bounding-box-tool/BoundingBoxTool.test.ts @@ -3,6 +3,8 @@ import { describe, it, expect } from 'vitest'; import { BoundingBoxTool } from '../../../src/tools/bounding-box-tool/BoundingBoxTool.js'; +import type { BoundingBoxOutput } from '../../../src/tools/bounding-box-tool/BoundingBoxTool.output.schema.js'; +import { structuredOf } from '../../utils/resultTestUtils.js'; describe('BoundingBoxTool', () => { const tool = new BoundingBoxTool(); @@ -11,9 +13,10 @@ describe('BoundingBoxTool', () => { const result = await tool.run({ geometry: [10, 20] }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.bbox).toEqual([10, 20, 10, 20]); + expect(data.bbox).toEqual([10, 20, 10, 20]); }); it('should calculate bbox for a linestring', async () => { @@ -24,9 +27,10 @@ describe('BoundingBoxTool', () => { [5, 10] ] }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.bbox).toEqual([0, 0, 10, 10]); + expect(data.bbox).toEqual([0, 0, 10, 10]); }); it('should calculate bbox for a polygon', async () => { @@ -41,9 +45,10 @@ describe('BoundingBoxTool', () => { ] ] }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.bbox).toEqual([0, 0, 10, 10]); + expect(data.bbox).toEqual([0, 0, 10, 10]); }); it('should calculate bbox for a polygon with hole', async () => { @@ -67,10 +72,11 @@ describe('BoundingBoxTool', () => { ] ] }); + const data = structuredOf(result); expect(result.isError).toBe(false); // BBox should be based on outer ring only - expect(result.structuredContent?.bbox).toEqual([-10, -10, 10, 10]); + expect(data.bbox).toEqual([-10, -10, 10, 10]); }); it('should calculate bbox for a multipolygon', async () => { @@ -98,10 +104,11 @@ describe('BoundingBoxTool', () => { ] ] }); + const data = structuredOf(result); expect(result.isError).toBe(false); // BBox should encompass both polygons - expect(result.structuredContent?.bbox).toEqual([0, 0, 20, 20]); + expect(data.bbox).toEqual([0, 0, 20, 20]); }); it('should calculate bbox for irregular polygon', async () => { @@ -117,9 +124,10 @@ describe('BoundingBoxTool', () => { ] ] }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.bbox).toEqual([1, 2, 9, 9]); + expect(data.bbox).toEqual([1, 2, 9, 9]); }); it('should calculate bbox for negative coordinates', async () => { @@ -134,9 +142,10 @@ describe('BoundingBoxTool', () => { ] ] }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.bbox).toEqual([-120, -30, -110, -20]); + expect(data.bbox).toEqual([-120, -30, -110, -20]); }); it('should calculate bbox crossing the equator and prime meridian', async () => { @@ -151,9 +160,10 @@ describe('BoundingBoxTool', () => { ] ] }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.bbox).toEqual([-10, -10, 10, 10]); + expect(data.bbox).toEqual([-10, -10, 10, 10]); }); it('should calculate bbox for real world coordinates (San Francisco)', async () => { @@ -168,12 +178,13 @@ describe('BoundingBoxTool', () => { ] ] }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.bbox[0]).toBeCloseTo(-122.52, 5); - expect(result.structuredContent?.bbox[1]).toBeCloseTo(37.7, 5); - expect(result.structuredContent?.bbox[2]).toBeCloseTo(-122.35, 5); - expect(result.structuredContent?.bbox[3]).toBeCloseTo(37.82, 5); + expect(data.bbox[0]).toBeCloseTo(-122.52, 5); + expect(data.bbox[1]).toBeCloseTo(37.7, 5); + expect(data.bbox[2]).toBeCloseTo(-122.35, 5); + expect(data.bbox[3]).toBeCloseTo(37.82, 5); }); it('should calculate bbox near poles', async () => { @@ -188,9 +199,10 @@ describe('BoundingBoxTool', () => { ] ] }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.bbox).toEqual([0, 85, 90, 89]); + expect(data.bbox).toEqual([0, 85, 90, 89]); }); it('should round bbox coordinates to 6 decimal places', async () => { @@ -205,9 +217,10 @@ describe('BoundingBoxTool', () => { ] ] }); + const data = structuredOf(result); expect(result.isError).toBe(false); - const bbox = result.structuredContent?.bbox as number[]; + const bbox = data.bbox as number[]; bbox.forEach((coord) => { const decimalPlaces = (coord.toString().split('.')[1] || '').length; expect(decimalPlaces).toBeLessThanOrEqual(6); @@ -226,9 +239,10 @@ describe('BoundingBoxTool', () => { ] ] }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.bbox).toEqual([0, 0, 0.0001, 0.0001]); + expect(data.bbox).toEqual([0, 0, 0.0001, 0.0001]); }); it('should calculate bbox for very large geometries', async () => { @@ -243,9 +257,10 @@ describe('BoundingBoxTool', () => { ] ] }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.bbox).toEqual([-180, -90, 180, 90]); + expect(data.bbox).toEqual([-180, -90, 180, 90]); }); it('should calculate bbox for diagonal line', async () => { @@ -255,9 +270,10 @@ describe('BoundingBoxTool', () => { [10, 10] ] }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.bbox).toEqual([0, 0, 10, 10]); + expect(data.bbox).toEqual([0, 0, 10, 10]); }); it('should calculate bbox for horizontal line', async () => { @@ -267,9 +283,10 @@ describe('BoundingBoxTool', () => { [10, 5] ] }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.bbox).toEqual([0, 5, 10, 5]); + expect(data.bbox).toEqual([0, 5, 10, 5]); }); it('should calculate bbox for vertical line', async () => { @@ -279,9 +296,10 @@ describe('BoundingBoxTool', () => { [5, 10] ] }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.bbox).toEqual([5, 0, 5, 10]); + expect(data.bbox).toEqual([5, 0, 5, 10]); }); it('should calculate bbox for zigzag linestring', async () => { @@ -294,9 +312,10 @@ describe('BoundingBoxTool', () => { [20, 0] ] }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.bbox).toEqual([0, 0, 20, 10]); + expect(data.bbox).toEqual([0, 0, 20, 10]); }); it('should handle triangle polygon', async () => { @@ -310,9 +329,10 @@ describe('BoundingBoxTool', () => { ] ] }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.bbox).toEqual([0, 0, 10, 10]); + expect(data.bbox).toEqual([0, 0, 10, 10]); }); it('should calculate bbox format as [minLon, minLat, maxLon, maxLat]', async () => { @@ -322,9 +342,10 @@ describe('BoundingBoxTool', () => { [30, 40] ] }); + const data = structuredOf(result); expect(result.isError).toBe(false); - const bbox = result.structuredContent?.bbox as number[]; + const bbox = data.bbox as number[]; // Verify format: [west, south, east, north] expect(bbox[0]).toBeLessThanOrEqual(bbox[2]); // minLon <= maxLon expect(bbox[1]).toBeLessThanOrEqual(bbox[3]); // minLat <= maxLat diff --git a/test/tools/buffer-tool/BufferTool.test.ts b/test/tools/buffer-tool/BufferTool.test.ts index 413ec0f8..8d369f69 100644 --- a/test/tools/buffer-tool/BufferTool.test.ts +++ b/test/tools/buffer-tool/BufferTool.test.ts @@ -3,6 +3,8 @@ import { describe, it, expect } from 'vitest'; import { BufferTool } from '../../../src/tools/buffer-tool/BufferTool.js'; +import type { BufferOutput } from '../../../src/tools/buffer-tool/BufferTool.output.schema.js'; +import { structuredOf } from '../../utils/resultTestUtils.js'; describe('BufferTool', () => { const tool = new BufferTool(); @@ -12,18 +14,17 @@ describe('BufferTool', () => { geometry: [0, 0], distance: 1 }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.distance).toBe(1); - expect(result.structuredContent?.units).toBe('kilometers'); - expect(result.structuredContent?.bufferedPolygon).toBeDefined(); - expect(Array.isArray(result.structuredContent?.bufferedPolygon)).toBe(true); + expect(data.distance).toBe(1); + expect(data.units).toBe('kilometers'); + expect(data.bufferedPolygon).toBeDefined(); + expect(Array.isArray(data.bufferedPolygon)).toBe(true); // Should have at least one ring - expect(result.structuredContent?.bufferedPolygon.length).toBeGreaterThan(0); + expect(data.bufferedPolygon.length).toBeGreaterThan(0); // Outer ring should have multiple vertices (typically 64 for a circle) - expect(result.structuredContent?.bufferedPolygon[0].length).toBeGreaterThan( - 10 - ); + expect(data.bufferedPolygon[0].length).toBeGreaterThan(10); }); it('should create buffer around a point in meters', async () => { @@ -32,11 +33,12 @@ describe('BufferTool', () => { distance: 1000, units: 'meters' }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.distance).toBe(1000); - expect(result.structuredContent?.units).toBe('meters'); - expect(result.structuredContent?.bufferedPolygon).toBeDefined(); + expect(data.distance).toBe(1000); + expect(data.units).toBe('meters'); + expect(data.bufferedPolygon).toBeDefined(); }); it('should create buffer around a point in miles', async () => { @@ -45,11 +47,12 @@ describe('BufferTool', () => { distance: 5, units: 'miles' }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.distance).toBe(5); - expect(result.structuredContent?.units).toBe('miles'); - expect(result.structuredContent?.bufferedPolygon).toBeDefined(); + expect(data.distance).toBe(5); + expect(data.units).toBe('miles'); + expect(data.bufferedPolygon).toBeDefined(); }); it('should create buffer around a point in feet', async () => { @@ -58,11 +61,12 @@ describe('BufferTool', () => { distance: 5000, units: 'feet' }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.distance).toBe(5000); - expect(result.structuredContent?.units).toBe('feet'); - expect(result.structuredContent?.bufferedPolygon).toBeDefined(); + expect(data.distance).toBe(5000); + expect(data.units).toBe('feet'); + expect(data.bufferedPolygon).toBeDefined(); }); it('should create buffer around a linestring', async () => { @@ -74,13 +78,12 @@ describe('BufferTool', () => { distance: 0.5, units: 'kilometers' }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.bufferedPolygon).toBeDefined(); + expect(data.bufferedPolygon).toBeDefined(); // Buffer around line should have more vertices than around point - expect(result.structuredContent?.bufferedPolygon[0].length).toBeGreaterThan( - 10 - ); + expect(data.bufferedPolygon[0].length).toBeGreaterThan(10); }); it('should create buffer around a multi-segment linestring', async () => { @@ -94,9 +97,10 @@ describe('BufferTool', () => { distance: 0.1, units: 'kilometers' }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.bufferedPolygon).toBeDefined(); + expect(data.bufferedPolygon).toBeDefined(); }); it('should create buffer around a polygon', async () => { @@ -113,13 +117,12 @@ describe('BufferTool', () => { distance: 0.5, units: 'kilometers' }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.bufferedPolygon).toBeDefined(); + expect(data.bufferedPolygon).toBeDefined(); // Buffer around polygon should expand it - expect(result.structuredContent?.bufferedPolygon[0].length).toBeGreaterThan( - 4 - ); + expect(data.bufferedPolygon[0].length).toBeGreaterThan(4); }); it('should create buffer around a small polygon', async () => { @@ -136,9 +139,10 @@ describe('BufferTool', () => { distance: 100, units: 'meters' }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.bufferedPolygon).toBeDefined(); + expect(data.bufferedPolygon).toBeDefined(); }); it('should handle different buffer distances', async () => { @@ -147,19 +151,21 @@ describe('BufferTool', () => { distance: 1, units: 'kilometers' }); + const result1Data = structuredOf(result1); const result2 = await tool.run({ geometry: [0, 0], distance: 5, units: 'kilometers' }); + const result2Data = structuredOf(result2); expect(result1.isError).toBe(false); expect(result2.isError).toBe(false); // Larger buffer should have polygon that extends further from center // We can't directly compare areas, but both should succeed - expect(result1.structuredContent?.bufferedPolygon).toBeDefined(); - expect(result2.structuredContent?.bufferedPolygon).toBeDefined(); + expect(result1Data.bufferedPolygon).toBeDefined(); + expect(result2Data.bufferedPolygon).toBeDefined(); }); it('should create buffer around real world location (NYC)', async () => { @@ -168,11 +174,12 @@ describe('BufferTool', () => { distance: 10, units: 'kilometers' }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.bufferedPolygon).toBeDefined(); + expect(data.bufferedPolygon).toBeDefined(); // Check that buffer is centered roughly around NYC coordinates - const firstCoord = result.structuredContent?.bufferedPolygon[0][0]; + const firstCoord = data.bufferedPolygon[0][0]; expect(firstCoord).toBeDefined(); // Coordinates should be within reasonable range of NYC if (firstCoord) { @@ -189,9 +196,10 @@ describe('BufferTool', () => { distance: 5, units: 'kilometers' }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.bufferedPolygon).toBeDefined(); + expect(data.bufferedPolygon).toBeDefined(); }); it('should create buffer near the equator', async () => { @@ -200,9 +208,10 @@ describe('BufferTool', () => { distance: 10, units: 'kilometers' }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.bufferedPolygon).toBeDefined(); + expect(data.bufferedPolygon).toBeDefined(); }); it('should create buffer with very small distance', async () => { @@ -211,9 +220,10 @@ describe('BufferTool', () => { distance: 10, units: 'meters' }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.bufferedPolygon).toBeDefined(); + expect(data.bufferedPolygon).toBeDefined(); }); it('should create buffer with large distance', async () => { @@ -222,9 +232,10 @@ describe('BufferTool', () => { distance: 100, units: 'kilometers' }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.bufferedPolygon).toBeDefined(); + expect(data.bufferedPolygon).toBeDefined(); }); it('should return closed polygon (first and last coordinates match)', async () => { @@ -233,9 +244,10 @@ describe('BufferTool', () => { distance: 1, units: 'kilometers' }); + const data = structuredOf(result); expect(result.isError).toBe(false); - const outerRing = result.structuredContent?.bufferedPolygon[0]; + const outerRing = data.bufferedPolygon[0]; expect(outerRing).toBeDefined(); if (outerRing) { const firstCoord = outerRing[0]; @@ -251,9 +263,10 @@ describe('BufferTool', () => { distance: 1, units: 'kilometers' }); + const data = structuredOf(result); expect(result.isError).toBe(false); - const outerRing = result.structuredContent?.bufferedPolygon[0]; + const outerRing = data.bufferedPolygon[0]; expect(outerRing).toBeDefined(); // Circle should have many vertices for smooth appearance (typically 64) if (outerRing) { @@ -274,9 +287,10 @@ describe('BufferTool', () => { distance: 0.1, units: 'kilometers' }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.bufferedPolygon).toBeDefined(); + expect(data.bufferedPolygon).toBeDefined(); }); it('should create buffer around irregular polygon', async () => { @@ -294,9 +308,10 @@ describe('BufferTool', () => { distance: 0.5, units: 'kilometers' }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.bufferedPolygon).toBeDefined(); + expect(data.bufferedPolygon).toBeDefined(); }); it('should handle buffer near poles', async () => { @@ -305,8 +320,9 @@ describe('BufferTool', () => { distance: 10, units: 'kilometers' }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.bufferedPolygon).toBeDefined(); + expect(data.bufferedPolygon).toBeDefined(); }); }); diff --git a/test/tools/centroid-tool/CentroidTool.test.ts b/test/tools/centroid-tool/CentroidTool.test.ts index f72a152a..8b18a228 100644 --- a/test/tools/centroid-tool/CentroidTool.test.ts +++ b/test/tools/centroid-tool/CentroidTool.test.ts @@ -3,6 +3,8 @@ import { describe, it, expect } from 'vitest'; import { CentroidTool } from '../../../src/tools/centroid-tool/CentroidTool.js'; +import type { CentroidOutput } from '../../../src/tools/centroid-tool/CentroidTool.output.schema.js'; +import { structuredOf } from '../../utils/resultTestUtils.js'; describe('CentroidTool', () => { const tool = new CentroidTool(); @@ -21,8 +23,9 @@ describe('CentroidTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.centroid.longitude).toBeCloseTo(0, 1); - expect(result.structuredContent?.centroid.latitude).toBeCloseTo(0, 1); + const data = structuredOf(result); + expect(data.centroid.longitude).toBeCloseTo(0, 1); + expect(data.centroid.latitude).toBeCloseTo(0, 1); }); it('should calculate centroid of a triangle', async () => { @@ -39,8 +42,9 @@ describe('CentroidTool', () => { expect(result.isError).toBe(false); // Centroid of triangle should be at (3, 2) - expect(result.structuredContent?.centroid.longitude).toBeCloseTo(3, 1); - expect(result.structuredContent?.centroid.latitude).toBeCloseTo(2, 1); + const data = structuredOf(result); + expect(data.centroid.longitude).toBeCloseTo(3, 1); + expect(data.centroid.latitude).toBeCloseTo(2, 1); }); it('should calculate centroid of a rectangle', async () => { @@ -58,8 +62,9 @@ describe('CentroidTool', () => { expect(result.isError).toBe(false); // Centroid should be at (5, 2.5) - expect(result.structuredContent?.centroid.longitude).toBeCloseTo(5, 1); - expect(result.structuredContent?.centroid.latitude).toBeCloseTo(2.5, 1); + const data = structuredOf(result); + expect(data.centroid.longitude).toBeCloseTo(5, 1); + expect(data.centroid.latitude).toBeCloseTo(2.5, 1); }); it('should calculate centroid of polygon with real coordinates', async () => { @@ -78,8 +83,9 @@ describe('CentroidTool', () => { expect(result.isError).toBe(false); // Centroid should be roughly in the middle of SF - expect(result.structuredContent?.centroid.longitude).toBeCloseTo(-122.4, 1); - expect(result.structuredContent?.centroid.latitude).toBeCloseTo(37.75, 1); + const data = structuredOf(result); + expect(data.centroid.longitude).toBeCloseTo(-122.4, 1); + expect(data.centroid.latitude).toBeCloseTo(37.75, 1); }); it('should calculate centroid of polygon with hole', async () => { @@ -106,8 +112,9 @@ describe('CentroidTool', () => { expect(result.isError).toBe(false); // Centroid should still be at center, even though there's a hole - expect(result.structuredContent?.centroid.longitude).toBeCloseTo(0, 1); - expect(result.structuredContent?.centroid.latitude).toBeCloseTo(0, 1); + const data = structuredOf(result); + expect(data.centroid.longitude).toBeCloseTo(0, 1); + expect(data.centroid.latitude).toBeCloseTo(0, 1); }); it('should calculate centroid of multipolygon', async () => { @@ -138,9 +145,10 @@ describe('CentroidTool', () => { expect(result.isError).toBe(false); // Centroid should be between the two squares - expect(result.structuredContent?.centroid.longitude).toBeGreaterThan(1); - expect(result.structuredContent?.centroid.longitude).toBeLessThan(4); - expect(result.structuredContent?.centroid.latitude).toBeCloseTo(1, 1); + const data = structuredOf(result); + expect(data.centroid.longitude).toBeGreaterThan(1); + expect(data.centroid.longitude).toBeLessThan(4); + expect(data.centroid.latitude).toBeCloseTo(1, 1); }); it('should handle polygon in southern hemisphere', async () => { @@ -157,8 +165,9 @@ describe('CentroidTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.centroid.longitude).toBeCloseTo(-55, 1); - expect(result.structuredContent?.centroid.latitude).toBeCloseTo(-25, 1); + const data = structuredOf(result); + expect(data.centroid.longitude).toBeCloseTo(-55, 1); + expect(data.centroid.latitude).toBeCloseTo(-25, 1); }); it('should handle L-shaped polygon', async () => { @@ -178,10 +187,11 @@ describe('CentroidTool', () => { expect(result.isError).toBe(false); // Centroid should be somewhere in the L shape - expect(result.structuredContent?.centroid.longitude).toBeGreaterThan(0); - expect(result.structuredContent?.centroid.longitude).toBeLessThan(2); - expect(result.structuredContent?.centroid.latitude).toBeGreaterThan(0); - expect(result.structuredContent?.centroid.latitude).toBeLessThan(2); + const data = structuredOf(result); + expect(data.centroid.longitude).toBeGreaterThan(0); + expect(data.centroid.longitude).toBeLessThan(2); + expect(data.centroid.latitude).toBeGreaterThan(0); + expect(data.centroid.latitude).toBeLessThan(2); }); it('should round coordinates to 6 decimal places', async () => { @@ -198,8 +208,9 @@ describe('CentroidTool', () => { }); expect(result.isError).toBe(false); - const lon = result.structuredContent?.centroid.longitude as number; - const lat = result.structuredContent?.centroid.latitude as number; + const data = structuredOf(result); + const lon = data.centroid.longitude; + const lat = data.centroid.latitude; const lonDecimals = (lon.toString().split('.')[1] || '').length; const latDecimals = (lat.toString().split('.')[1] || '').length; expect(lonDecimals).toBeLessThanOrEqual(6); @@ -223,8 +234,9 @@ describe('CentroidTool', () => { expect(result.isError).toBe(false); // Centroid should be at center - expect(result.structuredContent?.centroid.longitude).toBeCloseTo(0, 1); - expect(result.structuredContent?.centroid.latitude).toBeCloseTo(0, 1); + const data = structuredOf(result); + expect(data.centroid.longitude).toBeCloseTo(0, 1); + expect(data.centroid.latitude).toBeCloseTo(0, 1); }); it('should handle polygon crossing antimeridian', async () => { @@ -243,16 +255,11 @@ describe('CentroidTool', () => { expect(result.isError).toBe(false); // Centroid calculation may vary with antimeridian crossing // Just check it's a valid coordinate - expect(result.structuredContent?.centroid.latitude).toBeGreaterThanOrEqual( - -90 - ); - expect(result.structuredContent?.centroid.latitude).toBeLessThanOrEqual(90); - expect(result.structuredContent?.centroid.longitude).toBeGreaterThanOrEqual( - -180 - ); - expect(result.structuredContent?.centroid.longitude).toBeLessThanOrEqual( - 180 - ); + const data = structuredOf(result); + expect(data.centroid.latitude).toBeGreaterThanOrEqual(-90); + expect(data.centroid.latitude).toBeLessThanOrEqual(90); + expect(data.centroid.longitude).toBeGreaterThanOrEqual(-180); + expect(data.centroid.longitude).toBeLessThanOrEqual(180); }); it('should handle very small polygon', async () => { @@ -269,8 +276,9 @@ describe('CentroidTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.centroid.longitude).toBeCloseTo(0.0005, 4); - expect(result.structuredContent?.centroid.latitude).toBeCloseTo(0.0005, 4); + const data = structuredOf(result); + expect(data.centroid.longitude).toBeCloseTo(0.0005, 4); + expect(data.centroid.latitude).toBeCloseTo(0.0005, 4); }); it('should handle large polygon', async () => { @@ -287,8 +295,9 @@ describe('CentroidTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.centroid.longitude).toBeCloseTo(-100, 1); - expect(result.structuredContent?.centroid.latitude).toBeCloseTo(35, 1); + const data = structuredOf(result); + expect(data.centroid.longitude).toBeCloseTo(-100, 1); + expect(data.centroid.latitude).toBeCloseTo(35, 1); }); it('should handle complex irregular polygon', async () => { @@ -308,9 +317,10 @@ describe('CentroidTool', () => { expect(result.isError).toBe(false); // Centroid should be somewhere inside or near the polygon - expect(result.structuredContent?.centroid.longitude).toBeGreaterThan(0); - expect(result.structuredContent?.centroid.longitude).toBeLessThan(5); - expect(result.structuredContent?.centroid.latitude).toBeGreaterThan(0); - expect(result.structuredContent?.centroid.latitude).toBeLessThan(5); + const data = structuredOf(result); + expect(data.centroid.longitude).toBeGreaterThan(0); + expect(data.centroid.longitude).toBeLessThan(5); + expect(data.centroid.latitude).toBeGreaterThan(0); + expect(data.centroid.latitude).toBeLessThan(5); }); }); diff --git a/test/tools/convex-tool/ConvexTool.test.ts b/test/tools/convex-tool/ConvexTool.test.ts index 646b0697..12135091 100644 --- a/test/tools/convex-tool/ConvexTool.test.ts +++ b/test/tools/convex-tool/ConvexTool.test.ts @@ -3,6 +3,8 @@ import { describe, it, expect } from 'vitest'; import { ConvexTool } from '../../../src/tools/convex-tool/ConvexTool.js'; +import type { ConvexOutput } from '../../../src/tools/convex-tool/ConvexTool.output.schema.js'; +import { structuredOf } from '../../utils/resultTestUtils.js'; describe('ConvexTool', () => { const tool = new ConvexTool(); @@ -19,12 +21,12 @@ describe('ConvexTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.geometry).not.toBeNull(); - expect(result.structuredContent?.num_points).toBe(5); + expect(structuredOf(result).geometry).not.toBeNull(); + expect(structuredOf(result).num_points).toBe(5); // Hull of a square is a polygon - expect((result.structuredContent?.geometry as { type: string })?.type).toBe( - 'Polygon' - ); + expect( + (structuredOf(result).geometry as { type: string })?.type + ).toBe('Polygon'); }); it('returns null geometry for collinear points', async () => { @@ -37,8 +39,8 @@ describe('ConvexTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.geometry).toBeNull(); - expect(result.structuredContent?.num_points).toBe(3); + expect(structuredOf(result).geometry).toBeNull(); + expect(structuredOf(result).num_points).toBe(3); }); it('works with real-world points', async () => { @@ -54,8 +56,8 @@ describe('ConvexTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.geometry).not.toBeNull(); - expect(result.structuredContent?.num_points).toBe(5); + expect(structuredOf(result).geometry).not.toBeNull(); + expect(structuredOf(result).num_points).toBe(5); }); it('rejects fewer than three points', async () => { diff --git a/test/tools/destination-tool/DestinationTool.test.ts b/test/tools/destination-tool/DestinationTool.test.ts index a2b5ebcf..3c24061b 100644 --- a/test/tools/destination-tool/DestinationTool.test.ts +++ b/test/tools/destination-tool/DestinationTool.test.ts @@ -3,6 +3,8 @@ import { describe, it, expect } from 'vitest'; import { DestinationTool } from '../../../src/tools/destination-tool/DestinationTool.js'; +import type { DestinationOutput } from '../../../src/tools/destination-tool/DestinationTool.output.schema.js'; +import { structuredOf } from '../../utils/resultTestUtils.js'; describe('DestinationTool', () => { const tool = new DestinationTool(); @@ -17,8 +19,12 @@ describe('DestinationTool', () => { expect(result.isError).toBe(false); // Should end up roughly 1 degree north - expect(result.structuredContent?.destination.latitude).toBeCloseTo(1, 0); - expect(result.structuredContent?.destination.longitude).toBeCloseTo(0, 1); + expect( + structuredOf(result).destination.latitude + ).toBeCloseTo(1, 0); + expect( + structuredOf(result).destination.longitude + ).toBeCloseTo(0, 1); }); it('calculates destination heading east', async () => { @@ -30,8 +36,12 @@ describe('DestinationTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.destination.longitude).toBeGreaterThan(0); - expect(result.structuredContent?.destination.latitude).toBeCloseTo(0, 1); + expect( + structuredOf(result).destination.longitude + ).toBeGreaterThan(0); + expect( + structuredOf(result).destination.latitude + ).toBeCloseTo(0, 1); }); it('calculates destination in kilometers', async () => { @@ -44,10 +54,12 @@ describe('DestinationTool', () => { expect(result.isError).toBe(false); // 100km north should increase latitude by ~0.9 degrees - expect(result.structuredContent?.destination.latitude).toBeGreaterThan(38); - expect(result.structuredContent?.units).toBe('kilometers'); - expect(result.structuredContent?.distance).toBe(100); - expect(result.structuredContent?.bearing).toBe(0); + expect( + structuredOf(result).destination.latitude + ).toBeGreaterThan(38); + expect(structuredOf(result).units).toBe('kilometers'); + expect(structuredOf(result).distance).toBe(100); + expect(structuredOf(result).bearing).toBe(0); }); it('defaults to meters units', async () => { @@ -58,7 +70,7 @@ describe('DestinationTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.units).toBe('meters'); + expect(structuredOf(result).units).toBe('meters'); }); it('calculates destination heading south (negative bearing)', async () => { @@ -71,7 +83,9 @@ describe('DestinationTool', () => { expect(result.isError).toBe(false); // Heading south should decrease latitude - expect(result.structuredContent?.destination.latitude).toBeLessThan(10); + expect( + structuredOf(result).destination.latitude + ).toBeLessThan(10); }); it('rejects bearing out of range', async () => { diff --git a/test/tools/difference-tool/DifferenceTool.test.ts b/test/tools/difference-tool/DifferenceTool.test.ts index 26dc728a..8a3c9dc5 100644 --- a/test/tools/difference-tool/DifferenceTool.test.ts +++ b/test/tools/difference-tool/DifferenceTool.test.ts @@ -3,7 +3,8 @@ import { describe, it, expect } from 'vitest'; import { DifferenceTool } from '../../../src/tools/difference-tool/DifferenceTool.js'; -import { tokenFor } from '../../utils/tokenTestUtils.js'; +import type { DifferenceOutput } from '../../../src/tools/difference-tool/DifferenceTool.output.schema.js'; +import { structuredOf } from '../../utils/resultTestUtils.js'; describe('DifferenceTool', () => { const tool = new DifferenceTool(); @@ -31,8 +32,8 @@ describe('DifferenceTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.has_difference).toBe(true); - expect(result.structuredContent?.geometry).not.toBeNull(); + expect(structuredOf(result).has_difference).toBe(true); + expect(structuredOf(result).geometry).not.toBeNull(); }); it('returns has_difference=false when polygon2 fully covers polygon1', async () => { @@ -58,8 +59,8 @@ describe('DifferenceTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.has_difference).toBe(false); - expect(result.structuredContent?.geometry).toBeNull(); + expect(structuredOf(result).has_difference).toBe(false); + expect(structuredOf(result).geometry).toBeNull(); }); it('returns full polygon1 when polygons do not overlap', async () => { @@ -85,8 +86,8 @@ describe('DifferenceTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.has_difference).toBe(true); - expect(result.structuredContent?.geometry).not.toBeNull(); + expect(structuredOf(result).has_difference).toBe(true); + expect(structuredOf(result).geometry).not.toBeNull(); }); it('respects holes in polygon1', async () => { @@ -124,9 +125,9 @@ describe('DifferenceTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.has_difference).toBe(true); + expect(structuredOf(result).has_difference).toBe(true); // Result should still have a hole in it - expect(result.structuredContent?.geometry).not.toBeNull(); + expect(structuredOf(result).geometry).not.toBeNull(); }); it('text content describes the result', async () => { @@ -180,31 +181,26 @@ describe('DifferenceTool', () => { }); it('stores a mapboxRender payload with input fills + difference result', async () => { - const token = tokenFor('account-test-difference'); - const result = await tool.run( - { - polygon1: [ - [ - [0, 0], - [4, 0], - [4, 4], - [0, 4], - [0, 0] - ] - ], - polygon2: [ - [ - [1, 1], - [3, 1], - [3, 3], - [1, 3], - [1, 1] - ] + const result = await tool.run({ + polygon1: [ + [ + [0, 0], + [4, 0], + [4, 4], + [0, 4], + [0, 0] ] - }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - { http: { authInfo: { token } } } as any - ); + ], + polygon2: [ + [ + [1, 1], + [3, 1], + [3, 3], + [1, 3], + [1, 1] + ] + ] + }); expect(result.isError).toBe(false); const sc = result.structuredContent as { mapboxRender?: { ref?: string } }; diff --git a/test/tools/directions-tool/DirectionsTool.test.ts b/test/tools/directions-tool/DirectionsTool.test.ts index 1bb1c479..3dacd703 100644 --- a/test/tools/directions-tool/DirectionsTool.test.ts +++ b/test/tools/directions-tool/DirectionsTool.test.ts @@ -13,6 +13,7 @@ import * as cleanResponseModule from '../../../src/tools/directions-tool/cleanRe import { temporaryResourceManager } from '../../../src/utils/temporaryResourceManager.js'; import { tokenFor } from '../../utils/tokenTestUtils.js'; import { resolveResponseRef } from '../../../src/utils/responseRef.js'; +import type { HttpRequest } from '../../../src/utils/types.js'; describe('DirectionsTool', () => { beforeEach(() => { @@ -1157,8 +1158,8 @@ describe('DirectionsTool', () => { ) }; - return vi.fn(async (url: string) => { - const response = url.includes('geometries=geojson') + return vi.fn(async (input) => { + const response = String(input).includes('geometries=geojson') ? withGeometry : withoutGeometry; return { diff --git a/test/tools/directions-tool/cleanResponseData.test.ts b/test/tools/directions-tool/cleanResponseData.test.ts index a257bc96..8a30c918 100644 --- a/test/tools/directions-tool/cleanResponseData.test.ts +++ b/test/tools/directions-tool/cleanResponseData.test.ts @@ -3,18 +3,31 @@ import { describe, it, expect } from 'vitest'; import { cleanResponseData } from '../../../src/tools/directions-tool/cleanResponseData.js'; +import { DirectionsInputSchema } from '../../../src/tools/directions-tool/DirectionsTool.input.schema.js'; describe('cleanResponseData', () => { - // Create a complete mock input that satisfies the DirectionsInputSchema - const mockInput = { + // A full, parsed DirectionsInputSchema input. cleanResponseData only reads + // `geometries`. + const mockInput = DirectionsInputSchema.parse({ coordinates: [ - [0, 0], - [1, 1] - ] as [number, number][], // Use a proper tuple type - routing_profile: 'driving-traffic' as const, - geometries: 'none' as const, + { longitude: 0, latitude: 0 }, + { longitude: 1, latitude: 1 } + ], + routing_profile: 'mapbox/driving-traffic', + geometries: 'none', alternatives: false - }; + }); + + // The cleaned response types routes and waypoints as optional; these fail + // the test, rather than the type check, when they're missing. + function routesOf(result: ReturnType) { + if (!result.routes) throw new Error('result has no routes'); + return result.routes; + } + function waypointsOf(result: ReturnType) { + if (!result.waypoints) throw new Error('result has no waypoints'); + return result.waypoints; + } it('should remove unnecessary keys', () => { const mockData = { @@ -33,7 +46,7 @@ describe('cleanResponseData', () => { waypoints: [ { name: 'Test', - location: [-73.989, 40.733], + location: [-73.989, 40.733] as [number, number], distance: 10.5 } ], @@ -41,10 +54,10 @@ describe('cleanResponseData', () => { }; const result = cleanResponseData(mockInput, mockData); - expect(result.waypoints[0].location).toBeUndefined(); - expect(result.waypoints[0].snap_location).toEqual([-73.989, 40.733]); - expect(result.waypoints[0].distance).toBeUndefined(); - expect(result.waypoints[0].snap_distance).toBe(11); // Rounded from 10.5 + expect(waypointsOf(result)[0].location).toBeUndefined(); + expect(waypointsOf(result)[0].snap_location).toEqual([-73.989, 40.733]); + expect(waypointsOf(result)[0].distance).toBeUndefined(); + expect(waypointsOf(result)[0].snap_distance).toBe(11); // Rounded from 10.5 }); it('should round duration and distance', () => { @@ -59,8 +72,8 @@ describe('cleanResponseData', () => { }; const result = cleanResponseData(mockInput, mockData); - expect(result.routes[0].duration).toBe(1235); - expect(result.routes[0].distance).toBe(5679); + expect(routesOf(result)[0].duration).toBe(1235); + expect(routesOf(result)[0].distance).toBe(5679); }); it('should delete geometry when geometries is set to none', () => { @@ -74,7 +87,7 @@ describe('cleanResponseData', () => { }; const result = cleanResponseData(mockInput, mockData); - expect(result.routes[0].geometry).toBeUndefined(); + expect(routesOf(result)[0].geometry).toBeUndefined(); }); it('should keep geometry when geometries is not none', () => { @@ -94,7 +107,7 @@ describe('cleanResponseData', () => { }; const result = cleanResponseData(geojsonInput, mockData); - expect(result.routes[0].geometry).toBe('someGeometryData'); + expect(routesOf(result)[0].geometry).toBe('someGeometryData'); }); it('should process route data correctly', () => { @@ -126,20 +139,20 @@ describe('cleanResponseData', () => { const result = cleanResponseData(mockInput, mockData); // Check if data is properly processed - expect(result.routes[0].weight_name).toBeUndefined(); - expect(result.routes[0].weight).toBeUndefined(); - expect(result.routes[0].leg_summaries).toEqual([ + expect(routesOf(result)[0].weight_name).toBeUndefined(); + expect(routesOf(result)[0].weight).toBeUndefined(); + expect(routesOf(result)[0].leg_summaries).toEqual([ 'First leg summary', 'Second leg summary' ]); - expect(result.routes[0].duration_typical).toBeUndefined(); - expect(result.routes[0].duration_under_typical_traffic_conditions).toBe( + expect(routesOf(result)[0].duration_typical).toBeUndefined(); + expect(routesOf(result)[0].duration_under_typical_traffic_conditions).toBe( 1300 ); - expect(result.routes[0].weight_typical).toBeUndefined(); - expect(result.routes[0].intersecting_admins).toEqual(['USA', 'CAN']); - expect(result.routes[0].num_legs).toBe(2); - expect(result.routes[0].legs).toBeUndefined(); + expect(routesOf(result)[0].weight_typical).toBeUndefined(); + expect(routesOf(result)[0].intersecting_admins).toEqual(['USA', 'CAN']); + expect(routesOf(result)[0].num_legs).toBe(2); + expect(routesOf(result)[0].legs).toBeUndefined(); }); it('should handle notifications and collect unique messages', () => { @@ -168,7 +181,7 @@ describe('cleanResponseData', () => { const result = cleanResponseData(mockInput, mockData); - expect(result.routes[0].notifications_summary).toEqual([ + expect(routesOf(result)[0].notifications_summary).toEqual([ 'Traffic ahead', 'Road closure', 'Construction zone' @@ -201,8 +214,8 @@ describe('cleanResponseData', () => { const result = cleanResponseData(mockInput, mockData); - expect(result.routes[0].incidents_summary).toHaveLength(1); - expect(result.routes[0].incidents_summary[0]).toEqual({ + expect(routesOf(result)[0].incidents_summary).toHaveLength(1); + expect(routesOf(result)[0].incidents_summary![0]).toEqual({ type: 'accident', end_time: '2023-05-01T12:00:00Z', long_description: 'Multiple vehicle collision', @@ -210,7 +223,9 @@ describe('cleanResponseData', () => { affected_road_names: ['Main St'], length: 500 }); - expect(result.routes[0].incidents_summary[0].extra_field).toBeUndefined(); + expect(routesOf(result)[0].incidents_summary![0]).not.toHaveProperty( + 'extra_field' + ); }); it('should collect voice instructions when within limits', () => { @@ -241,7 +256,7 @@ describe('cleanResponseData', () => { const result = cleanResponseData(mockInput, mockData); - expect(result.routes[0].instructions).toEqual([ + expect(routesOf(result)[0].instructions).toEqual([ 'Turn right in 100 meters', 'Turn right now', 'Continue straight for 500 meters' @@ -273,7 +288,7 @@ describe('cleanResponseData', () => { // With 6 steps and 2 instructions each, we'd have 12 instructions total // The function should exclude them since it's > 10 - expect(result.routes[0].instructions).toBeUndefined(); + expect(routesOf(result)[0].instructions).toBeUndefined(); }); it('should calculate congestion information correctly', () => { @@ -295,7 +310,7 @@ describe('cleanResponseData', () => { const result = cleanResponseData(mockInput, mockData); - expect(result.routes[0].congestion_information).toEqual({ + expect(routesOf(result)[0].congestion_information).toEqual({ length_severe: 100, length_heavy: 200, length_moderate: 300, @@ -328,6 +343,6 @@ describe('cleanResponseData', () => { // Total distance = 100 + 200 + 300 = 600 // Average m/s = 14000/600 = 23.33 // Average km/h = 23.33 * 3.6 = 84 (rounded) - expect(result.routes[0].average_speed_kph).toBe(84); + expect(routesOf(result)[0].average_speed_kph).toBe(84); }); }); diff --git a/test/tools/distance-tool/DistanceTool.test.ts b/test/tools/distance-tool/DistanceTool.test.ts index 22f7e94a..1f724de2 100644 --- a/test/tools/distance-tool/DistanceTool.test.ts +++ b/test/tools/distance-tool/DistanceTool.test.ts @@ -3,6 +3,8 @@ import { describe, it, expect } from 'vitest'; import { DistanceTool } from '../../../src/tools/distance-tool/DistanceTool.js'; +import type { DistanceOutput } from '../../../src/tools/distance-tool/DistanceTool.output.schema.js'; +import { structuredOf } from '../../utils/resultTestUtils.js'; describe('DistanceTool', () => { const tool = new DistanceTool(); @@ -13,14 +15,15 @@ describe('DistanceTool', () => { to: { longitude: -118.2437, latitude: 34.0522 }, // Los Angeles units: 'kilometers' }); + const data = structuredOf(result); - expect(result.structuredContent?.distance).toBeCloseTo(559, 0); - expect(result.structuredContent?.units).toBe('kilometers'); - expect(result.structuredContent?.from).toEqual({ + expect(data.distance).toBeCloseTo(559, 0); + expect(data.units).toBe('kilometers'); + expect(data.from).toEqual({ longitude: -122.4194, latitude: 37.7749 }); - expect(result.structuredContent?.to).toEqual({ + expect(data.to).toEqual({ longitude: -118.2437, latitude: 34.0522 }); @@ -32,9 +35,10 @@ describe('DistanceTool', () => { to: { longitude: -118.2437, latitude: 34.0522 }, // Los Angeles units: 'miles' }); + const data = structuredOf(result); - expect(result.structuredContent?.distance).toBeCloseTo(347, 0); - expect(result.structuredContent?.units).toBe('miles'); + expect(data.distance).toBeCloseTo(347, 0); + expect(data.units).toBe('miles'); }); it('should calculate distance in meters', async () => { @@ -43,9 +47,10 @@ describe('DistanceTool', () => { to: { longitude: -118.2437, latitude: 34.0522 }, // Los Angeles units: 'meters' }); + const data = structuredOf(result); - expect(result.structuredContent?.distance).toBeGreaterThan(559000); - expect(result.structuredContent?.units).toBe('meters'); + expect(data.distance).toBeGreaterThan(559000); + expect(data.units).toBe('meters'); }); it('should default to kilometers when units not specified', async () => { @@ -53,8 +58,9 @@ describe('DistanceTool', () => { from: { longitude: -122.4194, latitude: 37.7749 }, to: { longitude: -118.2437, latitude: 34.0522 } }); + const data = structuredOf(result); - expect(result.structuredContent?.units).toBe('kilometers'); + expect(data.units).toBe('kilometers'); }); it('should calculate short distances accurately', async () => { @@ -63,8 +69,9 @@ describe('DistanceTool', () => { to: { longitude: 0, latitude: 0.01 }, // ~1.11 km units: 'meters' }); + const data = structuredOf(result); - expect(result.structuredContent?.distance).toBeCloseTo(1112, 0); + expect(data.distance).toBeCloseTo(1112, 0); }); it('should handle cross-hemisphere distances', async () => { @@ -73,8 +80,9 @@ describe('DistanceTool', () => { to: { longitude: 139.691706, latitude: 35.689487 }, // Tokyo units: 'kilometers' }); + const data = structuredOf(result); - expect(result.structuredContent?.distance).toBeCloseTo(10850, 0); + expect(data.distance).toBeCloseTo(10850, 0); }); it('should calculate distance for same point as zero', async () => { @@ -83,8 +91,9 @@ describe('DistanceTool', () => { to: { longitude: 0, latitude: 0 }, units: 'kilometers' }); + const data = structuredOf(result); - expect(result.structuredContent?.distance).toBe(0); + expect(data.distance).toBe(0); }); it('should round distance to 3 decimal places', async () => { @@ -93,8 +102,9 @@ describe('DistanceTool', () => { to: { longitude: 0.001, latitude: 0.001 }, units: 'kilometers' }); + const data = structuredOf(result); - const distance = result.structuredContent?.distance as number; + const distance = data.distance as number; const decimalPlaces = (distance.toString().split('.')[1] || '').length; expect(decimalPlaces).toBeLessThanOrEqual(3); }); diff --git a/test/tools/input-schema-validation.test.ts b/test/tools/input-schema-validation.test.ts index 4c239f01..8a04432d 100644 --- a/test/tools/input-schema-validation.test.ts +++ b/test/tools/input-schema-validation.test.ts @@ -27,26 +27,24 @@ function detectTupleUsage(schema: z.ZodType): string[] { ); } - // Traverse nested schemas + // Traverse nested schemas. In zod 4, ZodArray's _def.type is the string + // 'array', not the item schema, so use the public accessors. if (node instanceof z.ZodArray) { - if (node._def.type) { - traverse(node._def.type, `${path}[item]`); - } + traverse(node.element as z.ZodType, `${path}[item]`); } else if (node instanceof z.ZodObject) { - const shape = node._def.shape; - for (const [key, value] of Object.entries(shape)) { + for (const [key, value] of Object.entries(node.shape)) { traverse(value as z.ZodType, path ? `${path}.${key}` : key); } } else if (node instanceof z.ZodUnion) { - node._def.options.forEach((option: z.ZodType, index: number) => { - traverse(option, `${path}[union_option_${index}]`); + node.options.forEach((option, index) => { + traverse(option as z.ZodType, `${path}[union_option_${index}]`); }); - } else if (node instanceof z.ZodOptional) { - traverse(node._def.innerType, path); - } else if (node instanceof z.ZodDefault) { - traverse(node._def.innerType, path); - } else if (node instanceof z.ZodNullable) { - traverse(node._def.innerType, path); + } else if ( + node instanceof z.ZodOptional || + node instanceof z.ZodDefault || + node instanceof z.ZodNullable + ) { + traverse(node.unwrap() as z.ZodType, path); } } @@ -77,12 +75,14 @@ describe('Input Schema Validation - No Tuples', () => { coordinates: z.tuple([z.number(), z.number()]), data: z.object({ nestedTuple: z.tuple([z.string(), z.boolean()]) - }) + }), + path: z.array(z.tuple([z.number(), z.number()])).optional() }); const tupleIssues = detectTupleUsage(schemaWithTuple); - expect(tupleIssues).toHaveLength(2); + expect(tupleIssues).toHaveLength(3); expect(tupleIssues[0]).toContain('coordinates: z.tuple() detected'); expect(tupleIssues[1]).toContain('data.nestedTuple: z.tuple() detected'); + expect(tupleIssues[2]).toContain('path[item]: z.tuple() detected'); }); }); diff --git a/test/tools/intersect-tool/IntersectTool.test.ts b/test/tools/intersect-tool/IntersectTool.test.ts index 9e74f4eb..89656629 100644 --- a/test/tools/intersect-tool/IntersectTool.test.ts +++ b/test/tools/intersect-tool/IntersectTool.test.ts @@ -3,7 +3,8 @@ import { describe, it, expect } from 'vitest'; import { IntersectTool } from '../../../src/tools/intersect-tool/IntersectTool.js'; -import { tokenFor } from '../../utils/tokenTestUtils.js'; +import type { IntersectOutput } from '../../../src/tools/intersect-tool/IntersectTool.output.schema.js'; +import { structuredOf } from '../../utils/resultTestUtils.js'; describe('IntersectTool', () => { const tool = new IntersectTool(); @@ -31,8 +32,8 @@ describe('IntersectTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.intersects).toBe(true); - expect(result.structuredContent?.geometry).not.toBeNull(); + expect(structuredOf(result).intersects).toBe(true); + expect(structuredOf(result).geometry).not.toBeNull(); }); it('returns intersects=false for non-overlapping polygons', async () => { @@ -58,8 +59,8 @@ describe('IntersectTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.intersects).toBe(false); - expect(result.structuredContent?.geometry).toBeNull(); + expect(structuredOf(result).intersects).toBe(false); + expect(structuredOf(result).geometry).toBeNull(); }); it('returns intersection for one polygon fully inside another', async () => { @@ -85,9 +86,9 @@ describe('IntersectTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.intersects).toBe(true); + expect(structuredOf(result).intersects).toBe(true); // Intersection should equal the smaller polygon - expect(result.structuredContent?.geometry).not.toBeNull(); + expect(structuredOf(result).geometry).not.toBeNull(); }); it('respects holes when computing intersection', async () => { @@ -126,7 +127,7 @@ describe('IntersectTool', () => { expect(result.isError).toBe(false); // The hole excludes that area from polygon1, so no shared area - expect(result.structuredContent?.intersects).toBe(false); + expect(structuredOf(result).intersects).toBe(false); }); it('text content describes intersection result', async () => { @@ -180,31 +181,26 @@ describe('IntersectTool', () => { }); it('stores a mapboxRender payload with input fills + intersection result', async () => { - const token = tokenFor('account-test-intersect'); - const result = await tool.run( - { - polygon1: [ - [ - [0, 0], - [2, 0], - [2, 2], - [0, 2], - [0, 0] - ] - ], - polygon2: [ - [ - [1, 1], - [3, 1], - [3, 3], - [1, 3], - [1, 1] - ] + const result = await tool.run({ + polygon1: [ + [ + [0, 0], + [2, 0], + [2, 2], + [0, 2], + [0, 0] ] - }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - { http: { authInfo: { token } } } as any - ); + ], + polygon2: [ + [ + [1, 1], + [3, 1], + [3, 3], + [1, 3], + [1, 1] + ] + ] + }); expect(result.isError).toBe(false); const sc = result.structuredContent as { mapboxRender?: { ref?: string } }; diff --git a/test/tools/length-tool/LengthTool.test.ts b/test/tools/length-tool/LengthTool.test.ts index 86066ad4..d4da7d32 100644 --- a/test/tools/length-tool/LengthTool.test.ts +++ b/test/tools/length-tool/LengthTool.test.ts @@ -3,6 +3,8 @@ import { describe, it, expect } from 'vitest'; import { LengthTool } from '../../../src/tools/length-tool/LengthTool.js'; +import type { LengthOutput } from '../../../src/tools/length-tool/LengthTool.output.schema.js'; +import { structuredOf } from '../../utils/resultTestUtils.js'; describe('LengthTool', () => { const tool = new LengthTool(); @@ -17,9 +19,9 @@ describe('LengthTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.length).toBeCloseTo(111, 0); - expect(result.structuredContent?.units).toBe('kilometers'); - expect(result.structuredContent?.num_coordinates).toBe(2); + expect(structuredOf(result).length).toBeCloseTo(111, 0); + expect(structuredOf(result).units).toBe('kilometers'); + expect(structuredOf(result).num_coordinates).toBe(2); }); it('defaults to meters', async () => { @@ -31,8 +33,8 @@ describe('LengthTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.units).toBe('meters'); - expect(result.structuredContent?.length).toBeGreaterThan(0); + expect(structuredOf(result).units).toBe('meters'); + expect(structuredOf(result).length).toBeGreaterThan(0); }); it('measures in miles', async () => { @@ -45,8 +47,8 @@ describe('LengthTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.length).toBeCloseTo(347, 0); - expect(result.structuredContent?.units).toBe('miles'); + expect(structuredOf(result).length).toBeCloseTo(347, 0); + expect(structuredOf(result).units).toBe('miles'); }); it('accumulates length for multi-segment line', async () => { @@ -68,11 +70,11 @@ describe('LengthTool', () => { }); // Two-hop path through the same points should equal single hop - expect(twoHops.structuredContent?.length).toBeCloseTo( - singleHop.structuredContent?.length as number, + expect(structuredOf(twoHops).length).toBeCloseTo( + structuredOf(singleHop).length as number, 1 ); - expect(twoHops.structuredContent?.num_coordinates).toBe(3); + expect(structuredOf(twoHops).num_coordinates).toBe(3); }); it('rounds to 3 decimal places', async () => { @@ -85,7 +87,7 @@ describe('LengthTool', () => { }); expect(result.isError).toBe(false); - const len = result.structuredContent?.length as number; + const len = structuredOf(result).length as number; const decimals = (len.toString().split('.')[1] || '').length; expect(decimals).toBeLessThanOrEqual(3); }); diff --git a/test/tools/midpoint-tool/MidpointTool.test.ts b/test/tools/midpoint-tool/MidpointTool.test.ts index 95276a23..e5f5d6da 100644 --- a/test/tools/midpoint-tool/MidpointTool.test.ts +++ b/test/tools/midpoint-tool/MidpointTool.test.ts @@ -3,6 +3,8 @@ import { describe, it, expect } from 'vitest'; import { MidpointTool } from '../../../src/tools/midpoint-tool/MidpointTool.js'; +import type { MidpointOutput } from '../../../src/tools/midpoint-tool/MidpointTool.output.schema.js'; +import { structuredOf } from '../../utils/resultTestUtils.js'; describe('MidpointTool', () => { const tool = new MidpointTool(); @@ -14,13 +16,14 @@ describe('MidpointTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.midpoint.longitude).toBeCloseTo(5, 1); - expect(result.structuredContent?.midpoint.latitude).toBeCloseTo(0, 1); - expect(result.structuredContent?.from).toEqual({ + const data = structuredOf(result); + expect(data.midpoint.longitude).toBeCloseTo(5, 1); + expect(data.midpoint.latitude).toBeCloseTo(0, 1); + expect(data.from).toEqual({ longitude: 0, latitude: 0 }); - expect(result.structuredContent?.to).toEqual({ + expect(data.to).toEqual({ longitude: 10, latitude: 0 }); @@ -33,8 +36,9 @@ describe('MidpointTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.midpoint.longitude).toBeCloseTo(0, 1); - expect(result.structuredContent?.midpoint.latitude).toBeCloseTo(5, 1); + const data = structuredOf(result); + expect(data.midpoint.longitude).toBeCloseTo(0, 1); + expect(data.midpoint.latitude).toBeCloseTo(5, 1); }); it('should calculate midpoint for diagonal coordinates', async () => { @@ -44,8 +48,9 @@ describe('MidpointTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.midpoint.longitude).toBeCloseTo(5, 1); - expect(result.structuredContent?.midpoint.latitude).toBeCloseTo(5, 1); + const data = structuredOf(result); + expect(data.midpoint.longitude).toBeCloseTo(5, 1); + expect(data.midpoint.latitude).toBeCloseTo(5, 1); }); it('should calculate midpoint between San Francisco and Los Angeles', async () => { @@ -56,10 +61,11 @@ describe('MidpointTool', () => { expect(result.isError).toBe(false); // Midpoint should be roughly between the two cities - expect(result.structuredContent?.midpoint.longitude).toBeGreaterThan(-121); - expect(result.structuredContent?.midpoint.longitude).toBeLessThan(-119); - expect(result.structuredContent?.midpoint.latitude).toBeGreaterThan(35); - expect(result.structuredContent?.midpoint.latitude).toBeLessThan(37); + const data = structuredOf(result); + expect(data.midpoint.longitude).toBeGreaterThan(-121); + expect(data.midpoint.longitude).toBeLessThan(-119); + expect(data.midpoint.latitude).toBeGreaterThan(35); + expect(data.midpoint.latitude).toBeLessThan(37); }); it('should calculate midpoint between New York and London', async () => { @@ -70,10 +76,11 @@ describe('MidpointTool', () => { expect(result.isError).toBe(false); // Midpoint should be somewhere over the Atlantic - expect(result.structuredContent?.midpoint.longitude).toBeGreaterThan(-74); - expect(result.structuredContent?.midpoint.longitude).toBeLessThan(0); - expect(result.structuredContent?.midpoint.latitude).toBeGreaterThan(40); - expect(result.structuredContent?.midpoint.latitude).toBeLessThan(60); + const data = structuredOf(result); + expect(data.midpoint.longitude).toBeGreaterThan(-74); + expect(data.midpoint.longitude).toBeLessThan(0); + expect(data.midpoint.latitude).toBeGreaterThan(40); + expect(data.midpoint.latitude).toBeLessThan(60); }); it('should handle same point as same midpoint', async () => { @@ -83,8 +90,9 @@ describe('MidpointTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.midpoint.longitude).toBe(10); - expect(result.structuredContent?.midpoint.latitude).toBe(20); + const data = structuredOf(result); + expect(data.midpoint.longitude).toBe(10); + expect(data.midpoint.latitude).toBe(20); }); it('should handle midpoint across the antimeridian (180°)', async () => { @@ -95,10 +103,9 @@ describe('MidpointTool', () => { expect(result.isError).toBe(false); // Midpoint should be near 180° or -180° - expect( - Math.abs(result.structuredContent?.midpoint.longitude || 0) - ).toBeGreaterThan(175); - expect(result.structuredContent?.midpoint.latitude).toBeCloseTo(0, 1); + const data = structuredOf(result); + expect(Math.abs(data.midpoint.longitude)).toBeGreaterThan(175); + expect(data.midpoint.latitude).toBeCloseTo(0, 1); }); it('should handle midpoint near the north pole', async () => { @@ -108,8 +115,9 @@ describe('MidpointTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.midpoint.latitude).toBeGreaterThan(84); - expect(result.structuredContent?.midpoint.latitude).toBeLessThan(90); + const data = structuredOf(result); + expect(data.midpoint.latitude).toBeGreaterThan(84); + expect(data.midpoint.latitude).toBeLessThan(90); }); it('should handle midpoint in southern hemisphere', async () => { @@ -120,10 +128,11 @@ describe('MidpointTool', () => { expect(result.isError).toBe(false); // Midpoint should be between the two points - expect(result.structuredContent?.midpoint.longitude).toBeGreaterThan(-60); - expect(result.structuredContent?.midpoint.longitude).toBeLessThan(-50); - expect(result.structuredContent?.midpoint.latitude).toBeGreaterThan(-40); - expect(result.structuredContent?.midpoint.latitude).toBeLessThan(-30); + const data = structuredOf(result); + expect(data.midpoint.longitude).toBeGreaterThan(-60); + expect(data.midpoint.longitude).toBeLessThan(-50); + expect(data.midpoint.latitude).toBeGreaterThan(-40); + expect(data.midpoint.latitude).toBeLessThan(-30); }); it('should handle midpoint across hemispheres (N-S)', async () => { @@ -133,8 +142,9 @@ describe('MidpointTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.midpoint.longitude).toBeCloseTo(0, 1); - expect(result.structuredContent?.midpoint.latitude).toBeCloseTo(0, 1); + const data = structuredOf(result); + expect(data.midpoint.longitude).toBeCloseTo(0, 1); + expect(data.midpoint.latitude).toBeCloseTo(0, 1); }); it('should handle midpoint across hemispheres (E-W)', async () => { @@ -144,8 +154,9 @@ describe('MidpointTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.midpoint.longitude).toBeCloseTo(0, 1); - expect(result.structuredContent?.midpoint.latitude).toBeCloseTo(0, 1); + const data = structuredOf(result); + expect(data.midpoint.longitude).toBeCloseTo(0, 1); + expect(data.midpoint.latitude).toBeCloseTo(0, 1); }); it('should round coordinates to 6 decimal places', async () => { @@ -155,8 +166,9 @@ describe('MidpointTool', () => { }); expect(result.isError).toBe(false); - const lon = result.structuredContent?.midpoint.longitude as number; - const lat = result.structuredContent?.midpoint.latitude as number; + const data = structuredOf(result); + const lon = data.midpoint.longitude; + const lat = data.midpoint.latitude; const lonDecimals = (lon.toString().split('.')[1] || '').length; const latDecimals = (lat.toString().split('.')[1] || '').length; expect(lonDecimals).toBeLessThanOrEqual(6); @@ -170,7 +182,8 @@ describe('MidpointTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.midpoint.latitude).toBeCloseTo(0, 1); + const data = structuredOf(result); + expect(data.midpoint.latitude).toBeCloseTo(0, 1); }); it('should be commutative (same midpoint regardless of order)', async () => { @@ -184,14 +197,10 @@ describe('MidpointTool', () => { to: { longitude: 10, latitude: 20 } }); - expect(result1.structuredContent?.midpoint.longitude).toBeCloseTo( - result2.structuredContent?.midpoint.longitude || 0, - 5 - ); - expect(result1.structuredContent?.midpoint.latitude).toBeCloseTo( - result2.structuredContent?.midpoint.latitude || 0, - 5 - ); + const data1 = structuredOf(result1); + const data2 = structuredOf(result2); + expect(data1.midpoint.longitude).toBeCloseTo(data2.midpoint.longitude, 5); + expect(data1.midpoint.latitude).toBeCloseTo(data2.midpoint.latitude, 5); }); it('should handle short distances accurately', async () => { @@ -201,7 +210,8 @@ describe('MidpointTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.midpoint.longitude).toBeCloseTo(0.0005, 4); - expect(result.structuredContent?.midpoint.latitude).toBeCloseTo(0.0005, 4); + const data = structuredOf(result); + expect(data.midpoint.longitude).toBeCloseTo(0.0005, 4); + expect(data.midpoint.latitude).toBeCloseTo(0.0005, 4); }); }); diff --git a/test/tools/nearest-point-on-line-tool/NearestPointOnLineTool.test.ts b/test/tools/nearest-point-on-line-tool/NearestPointOnLineTool.test.ts index a5a766ed..84579a05 100644 --- a/test/tools/nearest-point-on-line-tool/NearestPointOnLineTool.test.ts +++ b/test/tools/nearest-point-on-line-tool/NearestPointOnLineTool.test.ts @@ -3,6 +3,8 @@ import { describe, it, expect } from 'vitest'; import { NearestPointOnLineTool } from '../../../src/tools/nearest-point-on-line-tool/NearestPointOnLineTool.js'; +import type { NearestPointOnLineOutput } from '../../../src/tools/nearest-point-on-line-tool/NearestPointOnLineTool.output.schema.js'; +import { structuredOf } from '../../utils/resultTestUtils.js'; describe('NearestPointOnLineTool', () => { const tool = new NearestPointOnLineTool(); @@ -19,9 +21,15 @@ describe('NearestPointOnLineTool', () => { expect(result.isError).toBe(false); // Nearest point on the line should be directly below the input point - expect(result.structuredContent?.nearest.longitude).toBeCloseTo(0.5, 1); - expect(result.structuredContent?.nearest.latitude).toBeCloseTo(0, 1); - expect(result.structuredContent?.distance).toBeGreaterThan(0); + expect( + structuredOf(result).nearest.longitude + ).toBeCloseTo(0.5, 1); + expect( + structuredOf(result).nearest.latitude + ).toBeCloseTo(0, 1); + expect( + structuredOf(result).distance + ).toBeGreaterThan(0); }); it('returns zero distance when point is on the line', async () => { @@ -35,7 +43,10 @@ describe('NearestPointOnLineTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.distance).toBeCloseTo(0, 1); + expect(structuredOf(result).distance).toBeCloseTo( + 0, + 1 + ); }); it('defaults to kilometers units', async () => { @@ -48,7 +59,9 @@ describe('NearestPointOnLineTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.units).toBe('kilometers'); + expect(structuredOf(result).units).toBe( + 'kilometers' + ); }); it('returns distance in meters when specified', async () => { @@ -70,11 +83,11 @@ describe('NearestPointOnLineTool', () => { units: 'meters' }); - expect(m.structuredContent?.distance).toBeCloseTo( - (km.structuredContent?.distance as number) * 1000, + expect(structuredOf(m).distance).toBeCloseTo( + (structuredOf(km).distance as number) * 1000, 0 ); - expect(m.structuredContent?.units).toBe('meters'); + expect(structuredOf(m).units).toBe('meters'); }); it('returns location along the line', async () => { @@ -90,7 +103,10 @@ describe('NearestPointOnLineTool', () => { expect(result.isError).toBe(false); // location should be roughly half the line length const lineLength = 111; // ~111km for 1 degree at equator - expect(result.structuredContent?.location).toBeCloseTo(lineLength / 2, -1); + expect(structuredOf(result).location).toBeCloseTo( + lineLength / 2, + -1 + ); }); it('works with a multi-segment line', async () => { @@ -106,8 +122,12 @@ describe('NearestPointOnLineTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.nearest).toBeDefined(); - expect(result.structuredContent?.distance).toBeGreaterThanOrEqual(0); + expect( + structuredOf(result).nearest + ).toBeDefined(); + expect( + structuredOf(result).distance + ).toBeGreaterThanOrEqual(0); }); it('rejects a line with fewer than two coordinates', async () => { diff --git a/test/tools/nearest-point-tool/NearestPointTool.test.ts b/test/tools/nearest-point-tool/NearestPointTool.test.ts index 2f20f5fc..85c93bf7 100644 --- a/test/tools/nearest-point-tool/NearestPointTool.test.ts +++ b/test/tools/nearest-point-tool/NearestPointTool.test.ts @@ -3,6 +3,8 @@ import { describe, it, expect } from 'vitest'; import { NearestPointTool } from '../../../src/tools/nearest-point-tool/NearestPointTool.js'; +import type { NearestPointOutput } from '../../../src/tools/nearest-point-tool/NearestPointTool.output.schema.js'; +import { structuredOf } from '../../utils/resultTestUtils.js'; describe('NearestPointTool', () => { const tool = new NearestPointTool(); @@ -18,11 +20,11 @@ describe('NearestPointTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.nearest).toMatchObject({ + expect(structuredOf(result).nearest).toMatchObject({ longitude: 1, latitude: 0 }); - expect(result.structuredContent?.index).toBe(0); + expect(structuredOf(result).index).toBe(0); }); it('returns correct index for nearest point', async () => { @@ -36,8 +38,8 @@ describe('NearestPointTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.index).toBe(2); - expect(result.structuredContent?.nearest).toMatchObject({ + expect(structuredOf(result).index).toBe(2); + expect(structuredOf(result).nearest).toMatchObject({ longitude: 0.5, latitude: 0 }); @@ -50,8 +52,10 @@ describe('NearestPointTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.units).toBe('meters'); - expect(result.structuredContent?.distance).toBeGreaterThan(0); + expect(structuredOf(result).units).toBe('meters'); + expect(structuredOf(result).distance).toBeGreaterThan( + 0 + ); }); it('returns distance in kilometers when specified', async () => { @@ -65,10 +69,13 @@ describe('NearestPointTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.units).toBe('kilometers'); + expect(structuredOf(result).units).toBe('kilometers'); // LA is closer to SF than NY - expect(result.structuredContent?.index).toBe(0); - expect(result.structuredContent?.distance).toBeCloseTo(559, 0); + expect(structuredOf(result).index).toBe(0); + expect(structuredOf(result).distance).toBeCloseTo( + 559, + 0 + ); }); it('handles a single candidate point', async () => { @@ -78,7 +85,7 @@ describe('NearestPointTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.index).toBe(0); + expect(structuredOf(result).index).toBe(0); }); it('rejects empty points array', async () => { diff --git a/test/tools/optimization-tool/OptimizationTool.test.ts b/test/tools/optimization-tool/OptimizationTool.test.ts index 60d46823..75110782 100644 --- a/test/tools/optimization-tool/OptimizationTool.test.ts +++ b/test/tools/optimization-tool/OptimizationTool.test.ts @@ -9,6 +9,7 @@ import { setupHttpRequest } from '../../utils/httpPipelineUtils.js'; import { OptimizationTool } from '../../../src/tools/optimization-tool/OptimizationTool.js'; import { tokenFor } from '../../utils/tokenTestUtils.js'; +import { textOf } from '../../utils/resultTestUtils.js'; // Sample V1 API response const sampleV1Response = { @@ -178,9 +179,7 @@ describe('OptimizationTool V1 API', () => { }); expect(result.isError).toBe(true); - expect(result.content[0].text).toContain( - 'At least 2 coordinates are required' - ); + expect(textOf(result)).toContain('At least 2 coordinates are required'); }); it('should validate maximum coordinates (12)', async () => { @@ -199,7 +198,7 @@ describe('OptimizationTool V1 API', () => { const result = await tool.run({ coordinates: coords }); expect(result.isError).toBe(true); - expect(result.content[0].text).toContain('Maximum 12 coordinates allowed'); + expect(textOf(result)).toContain('Maximum 12 coordinates allowed'); }); it('should handle API errors gracefully', async () => { @@ -222,8 +221,8 @@ describe('OptimizationTool V1 API', () => { }); expect(result.isError).toBe(true); - expect(result.content[0].text).toContain('Optimization API error'); - expect(result.content[0].text).toContain('Invalid coordinates'); + expect(textOf(result)).toContain('Optimization API error'); + expect(textOf(result)).toContain('Invalid coordinates'); }); it('should handle API error codes in response', async () => { @@ -245,7 +244,7 @@ describe('OptimizationTool V1 API', () => { }); expect(result.isError).toBe(true); - expect(result.content[0].text).toContain('No route found'); + expect(textOf(result)).toContain('No route found'); }); it('should format output text with duration and distance', async () => { @@ -265,7 +264,7 @@ describe('OptimizationTool V1 API', () => { }); expect(result.isError).toBe(false); - const text = result.content[0].text; + const text = textOf(result); expect(text).toContain('Optimized route'); expect(text).toContain('3 waypoints'); expect(text).toContain('minutes'); @@ -301,7 +300,7 @@ describe('OptimizationTool V1 API', () => { /^mapbox:\/\/selffetch\/optimization\?data=/ ); - const text = (result.content[0] as { text: string }).text; + const text = textOf(result); expect(text).toContain('render_map_tool'); expect(text).toContain(sc.mapboxRender!.ref!); diff --git a/test/tools/points-within-polygon-tool/PointsWithinPolygonTool.test.ts b/test/tools/points-within-polygon-tool/PointsWithinPolygonTool.test.ts index a694b9d1..278847fc 100644 --- a/test/tools/points-within-polygon-tool/PointsWithinPolygonTool.test.ts +++ b/test/tools/points-within-polygon-tool/PointsWithinPolygonTool.test.ts @@ -3,6 +3,8 @@ import { describe, it, expect } from 'vitest'; import { PointsWithinPolygonTool } from '../../../src/tools/points-within-polygon-tool/PointsWithinPolygonTool.js'; +import type { PointsWithinPolygonOutput } from '../../../src/tools/points-within-polygon-tool/PointsWithinPolygonTool.output.schema.js'; +import { structuredOf } from '../../utils/resultTestUtils.js'; // Simple unit square polygon [0,0] → [1,1] const SQUARE = [ @@ -26,11 +28,12 @@ describe('PointsWithinPolygonTool', () => { ], polygon: SQUARE }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.count).toBe(1); - expect(result.structuredContent?.total).toBe(2); - expect(result.structuredContent?.points_within[0]).toMatchObject({ + expect(data.count).toBe(1); + expect(data.total).toBe(2); + expect(data.points_within[0]).toMatchObject({ longitude: 0.5, latitude: 0.5 }); @@ -45,10 +48,11 @@ describe('PointsWithinPolygonTool', () => { ], polygon: SQUARE }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.count).toBe(3); - expect(result.structuredContent?.total).toBe(3); + expect(data.count).toBe(3); + expect(data.total).toBe(3); }); it('returns empty when no points are inside', async () => { @@ -59,11 +63,12 @@ describe('PointsWithinPolygonTool', () => { ], polygon: SQUARE }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.count).toBe(0); - expect(result.structuredContent?.total).toBe(2); - expect(result.structuredContent?.points_within).toHaveLength(0); + expect(data.count).toBe(0); + expect(data.total).toBe(2); + expect(data.points_within).toHaveLength(0); }); it('handles a single point inside', async () => { @@ -71,9 +76,10 @@ describe('PointsWithinPolygonTool', () => { points: [{ longitude: 0.5, latitude: 0.5 }], polygon: SQUARE }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.count).toBe(1); + expect(data.count).toBe(1); }); it('works with a real-world polygon', async () => { @@ -95,9 +101,10 @@ describe('PointsWithinPolygonTool', () => { ], polygon: manhattan }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.count).toBe(1); + expect(data.count).toBe(1); }); it('excludes points that fall inside a hole', async () => { @@ -127,14 +134,13 @@ describe('PointsWithinPolygonTool', () => { ], polygon: polygonWithHole }); + const data = structuredOf(result); expect(result.isError).toBe(false); - expect(result.structuredContent?.count).toBe(2); - expect(result.structuredContent?.total).toBe(3); + expect(data.count).toBe(2); + expect(data.total).toBe(3); // The hole point (2,2) should not be returned - const lngs = result.structuredContent?.points_within.map( - (p) => p.longitude - ); + const lngs = data.points_within.map((p) => p.longitude); expect(lngs).not.toContain(2); }); diff --git a/test/tools/simplify-tool/SimplifyTool.test.ts b/test/tools/simplify-tool/SimplifyTool.test.ts index 33635278..37509150 100644 --- a/test/tools/simplify-tool/SimplifyTool.test.ts +++ b/test/tools/simplify-tool/SimplifyTool.test.ts @@ -3,6 +3,8 @@ import { describe, it, expect } from 'vitest'; import { SimplifyTool } from '../../../src/tools/simplify-tool/SimplifyTool.js'; +import type { SimplifyOutput } from '../../../src/tools/simplify-tool/SimplifyTool.output.schema.js'; +import { structuredOf } from '../../utils/resultTestUtils.js'; describe('SimplifyTool', () => { const tool = new SimplifyTool(); @@ -26,11 +28,12 @@ describe('SimplifyTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.originalVertexCount).toBe(11); - expect(result.structuredContent?.simplifiedVertexCount).toBeLessThan(11); - expect(result.structuredContent?.reductionPercentage).toBeGreaterThan(0); - expect(result.structuredContent?.simplified).toBeDefined(); - expect(Array.isArray(result.structuredContent?.simplified)).toBe(true); + const data = structuredOf(result); + expect(data.originalVertexCount).toBe(11); + expect(data.simplifiedVertexCount).toBeLessThan(11); + expect(data.reductionPercentage).toBeGreaterThan(0); + expect(data.simplified).toBeDefined(); + expect(Array.isArray(data.simplified)).toBe(true); }); it('should simplify a linestring with custom tolerance', async () => { @@ -49,8 +52,9 @@ describe('SimplifyTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.originalVertexCount).toBe(6); - expect(result.structuredContent?.simplifiedVertexCount).toBeLessThan(6); + const data = structuredOf(result); + expect(data.originalVertexCount).toBe(6); + expect(data.simplifiedVertexCount).toBeLessThan(6); }); it('should simplify more aggressively with higher tolerance', async () => { @@ -77,10 +81,10 @@ describe('SimplifyTool', () => { expect(resultLow.isError).toBe(false); expect(resultHigh.isError).toBe(false); // Higher tolerance should result in fewer vertices - expect( - resultHigh.structuredContent?.simplifiedVertexCount - ).toBeLessThanOrEqual( - resultLow.structuredContent?.simplifiedVertexCount || Infinity + const dataHigh = structuredOf(resultHigh); + const dataLow = structuredOf(resultLow); + expect(dataHigh.simplifiedVertexCount).toBeLessThanOrEqual( + dataLow.simplifiedVertexCount ); }); @@ -103,11 +107,12 @@ describe('SimplifyTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.originalVertexCount).toBe(9); - expect(result.structuredContent?.simplifiedVertexCount).toBeLessThan(9); - expect(result.structuredContent?.simplified).toBeDefined(); + const data = structuredOf(result); + expect(data.originalVertexCount).toBe(9); + expect(data.simplifiedVertexCount).toBeLessThan(9); + expect(data.simplified).toBeDefined(); // Result should still be a polygon (array of array of coordinates) - expect(Array.isArray(result.structuredContent?.simplified)).toBe(true); + expect(Array.isArray(data.simplified)).toBe(true); }); it('should use high quality mode', async () => { @@ -126,12 +131,9 @@ describe('SimplifyTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.simplifiedVertexCount).toBeLessThanOrEqual( - 5 - ); - expect( - result.structuredContent?.simplifiedVertexCount - ).toBeGreaterThanOrEqual(2); + const data = structuredOf(result); + expect(data.simplifiedVertexCount).toBeLessThanOrEqual(5); + expect(data.simplifiedVertexCount).toBeGreaterThanOrEqual(2); }); it('should handle straight line (minimal simplification needed)', async () => { @@ -147,10 +149,11 @@ describe('SimplifyTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.originalVertexCount).toBe(5); + const data = structuredOf(result); + expect(data.originalVertexCount).toBe(5); // Straight line should simplify to just endpoints - expect(result.structuredContent?.simplifiedVertexCount).toBe(2); - expect(result.structuredContent?.reductionPercentage).toBe(60); + expect(data.simplifiedVertexCount).toBe(2); + expect(data.reductionPercentage).toBe(60); }); it('should handle complex curved line', async () => { @@ -166,9 +169,10 @@ describe('SimplifyTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.originalVertexCount).toBe(21); - expect(result.structuredContent?.simplifiedVertexCount).toBeLessThan(21); - expect(result.structuredContent?.reductionPercentage).toBeGreaterThan(0); + const data = structuredOf(result); + expect(data.originalVertexCount).toBe(21); + expect(data.simplifiedVertexCount).toBeLessThan(21); + expect(data.reductionPercentage).toBeGreaterThan(0); }); it('should calculate reduction percentage correctly', async () => { @@ -184,14 +188,13 @@ describe('SimplifyTool', () => { }); expect(result.isError).toBe(false); - const original = result.structuredContent?.originalVertexCount || 0; - const simplified = result.structuredContent?.simplifiedVertexCount || 0; + const data = structuredOf(result); + const original = data.originalVertexCount; + const simplified = data.simplifiedVertexCount; const expectedReduction = Math.round( ((original - simplified) / original) * 100 ); - expect(result.structuredContent?.reductionPercentage).toBe( - expectedReduction - ); + expect(data.reductionPercentage).toBe(expectedReduction); }); it('should preserve first and last points', async () => { @@ -209,10 +212,8 @@ describe('SimplifyTool', () => { }); expect(result.isError).toBe(false); - const simplified = result.structuredContent?.simplified as [ - number, - number - ][]; + const data = structuredOf(result); + const simplified = data.simplified as [number, number][]; expect(simplified[0]).toEqual([0, 0]); expect(simplified[simplified.length - 1]).toEqual([4, 0]); }); @@ -231,7 +232,8 @@ describe('SimplifyTool', () => { expect(result.isError).toBe(false); // Very small tolerance should preserve most vertices - expect(result.structuredContent?.simplifiedVertexCount).toBeGreaterThan(2); + const data = structuredOf(result); + expect(data.simplifiedVertexCount).toBeGreaterThan(2); }); it('should handle polygon with hole', async () => { @@ -267,8 +269,9 @@ describe('SimplifyTool', () => { expect(result.isError).toBe(false); // Original: 9 + 9 = 18 vertices - expect(result.structuredContent?.originalVertexCount).toBe(18); - expect(result.structuredContent?.simplifiedVertexCount).toBeLessThan(18); + const data = structuredOf(result); + expect(data.originalVertexCount).toBe(18); + expect(data.simplifiedVertexCount).toBeLessThan(18); }); it('should handle real world coordinates', async () => { @@ -284,10 +287,9 @@ describe('SimplifyTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.originalVertexCount).toBe(5); - expect(result.structuredContent?.simplifiedVertexCount).toBeLessThanOrEqual( - 5 - ); + const data = structuredOf(result); + expect(data.originalVertexCount).toBe(5); + expect(data.simplifiedVertexCount).toBeLessThanOrEqual(5); }); it('should handle square polygon (should simplify to 5 points including closure)', async () => { @@ -305,10 +307,11 @@ describe('SimplifyTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.originalVertexCount).toBe(5); + const data = structuredOf(result); + expect(data.originalVertexCount).toBe(5); // Perfect square should not simplify much (all corners are significant) - expect(result.structuredContent?.simplifiedVertexCount).toBe(5); - expect(result.structuredContent?.reductionPercentage).toBe(0); + expect(data.simplifiedVertexCount).toBe(5); + expect(data.reductionPercentage).toBe(0); }); it('should handle triangle (minimal vertices, no simplification)', async () => { @@ -325,10 +328,11 @@ describe('SimplifyTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.originalVertexCount).toBe(4); + const data = structuredOf(result); + expect(data.originalVertexCount).toBe(4); // Triangle should not simplify (all vertices are needed) - expect(result.structuredContent?.simplifiedVertexCount).toBe(4); - expect(result.structuredContent?.reductionPercentage).toBe(0); + expect(data.simplifiedVertexCount).toBe(4); + expect(data.reductionPercentage).toBe(0); }); it('should handle negative coordinates', async () => { @@ -344,7 +348,8 @@ describe('SimplifyTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.simplifiedVertexCount).toBeLessThan(5); + const data = structuredOf(result); + expect(data.simplifiedVertexCount).toBeLessThan(5); }); it('should return simplified geometry in same format as input', async () => { @@ -358,10 +363,9 @@ describe('SimplifyTool', () => { }); expect(lineResult.isError).toBe(false); - expect(Array.isArray(lineResult.structuredContent?.simplified)).toBe(true); - expect(Array.isArray(lineResult.structuredContent?.simplified[0])).toBe( - true - ); + const lineData = structuredOf(lineResult); + expect(Array.isArray(lineData.simplified)).toBe(true); + expect(Array.isArray(lineData.simplified[0])).toBe(true); // Test polygon const polyResult = await tool.run({ @@ -377,9 +381,8 @@ describe('SimplifyTool', () => { }); expect(polyResult.isError).toBe(false); - expect(Array.isArray(polyResult.structuredContent?.simplified)).toBe(true); - expect(Array.isArray(polyResult.structuredContent?.simplified[0])).toBe( - true - ); + const polyData = structuredOf(polyResult); + expect(Array.isArray(polyData.simplified)).toBe(true); + expect(Array.isArray(polyData.simplified[0])).toBe(true); }); }); diff --git a/test/tools/structured-content.test.ts b/test/tools/structured-content.test.ts index 75c4dfea..ad98094d 100644 --- a/test/tools/structured-content.test.ts +++ b/test/tools/structured-content.test.ts @@ -3,8 +3,10 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'; import { MapboxApiBasedTool } from '../../src/tools/MapboxApiBasedTool.js'; -import type { OutputSchema } from '../../src/tools/MapboxApiBasedTool.output.schema.js'; +import type { CallToolResult } from '@modelcontextprotocol/server'; import { z } from 'zod'; +import { setupHttpRequest } from '../utils/httpPipelineUtils.js'; +import { structuredOf } from '../utils/resultTestUtils.js'; const TestInputSchema = z.object({ test: z.string() @@ -22,13 +24,16 @@ class TestTool extends MapboxApiBasedTool { }; constructor() { - super({ inputSchema: TestInputSchema }); + super({ + inputSchema: TestInputSchema, + httpRequest: setupHttpRequest().httpRequest + }); } protected async execute( input: z.infer, _accessToken: string - ): Promise> { + ): Promise { // Return different types based on input if (input.test === 'object') { const data = { @@ -90,7 +95,7 @@ describe('MapboxApiBasedTool Structured Content', () => { 'This is structured content' ); expect(result.structuredContent).toHaveProperty('data'); - expect(result.structuredContent?.data).toEqual({ + expect(structuredOf<{ data: unknown }>(result).data).toEqual({ value: 42, success: true }); diff --git a/test/tools/union-tool/UnionTool.test.ts b/test/tools/union-tool/UnionTool.test.ts index 10339619..12c6a88d 100644 --- a/test/tools/union-tool/UnionTool.test.ts +++ b/test/tools/union-tool/UnionTool.test.ts @@ -3,7 +3,8 @@ import { describe, it, expect } from 'vitest'; import { UnionTool } from '../../../src/tools/union-tool/UnionTool.js'; -import { tokenFor } from '../../utils/tokenTestUtils.js'; +import type { UnionOutput } from '../../../src/tools/union-tool/UnionTool.output.schema.js'; +import { structuredOf } from '../../utils/resultTestUtils.js'; describe('UnionTool', () => { const tool = new UnionTool(); @@ -35,8 +36,8 @@ describe('UnionTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.type).toBe('Polygon'); - expect(result.structuredContent?.geometry).not.toBeNull(); + expect(structuredOf(result).type).toBe('Polygon'); + expect(structuredOf(result).geometry).not.toBeNull(); }); it('returns MultiPolygon for non-overlapping polygons', async () => { @@ -66,7 +67,7 @@ describe('UnionTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.type).toBe('MultiPolygon'); + expect(structuredOf(result).type).toBe('MultiPolygon'); }); it('merges three polygons', async () => { @@ -103,7 +104,7 @@ describe('UnionTool', () => { }); expect(result.isError).toBe(false); - expect(result.structuredContent?.geometry).not.toBeNull(); + expect(structuredOf(result).geometry).not.toBeNull(); }); it('handles polygons with holes', async () => { @@ -141,8 +142,8 @@ describe('UnionTool', () => { expect(result.isError).toBe(false); // Union should fill the hole, resulting in a solid polygon with no holes - expect(result.structuredContent?.type).toBe('Polygon'); - expect(result.structuredContent?.geometry).not.toBeNull(); + expect(structuredOf(result).type).toBe('Polygon'); + expect(structuredOf(result).geometry).not.toBeNull(); }); it('rejects fewer than two polygons', async () => { @@ -164,33 +165,28 @@ describe('UnionTool', () => { }); it('stores a mapboxRender payload with input fills + result fill', async () => { - const token = tokenFor('account-test-union'); - const result = await tool.run( - { - polygons: [ + const result = await tool.run({ + polygons: [ + [ [ - [ - [0, 0], - [2, 0], - [2, 2], - [0, 2], - [0, 0] - ] - ], + [0, 0], + [2, 0], + [2, 2], + [0, 2], + [0, 0] + ] + ], + [ [ - [ - [1, 1], - [3, 1], - [3, 3], - [1, 3], - [1, 1] - ] + [1, 1], + [3, 1], + [3, 3], + [1, 3], + [1, 1] ] ] - }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - { http: { authInfo: { token } } } as any - ); + ] + }); expect(result.isError).toBe(false); const sc = result.structuredContent as { mapboxRender?: { ref?: string } }; diff --git a/test/utils/directionsRef.test.ts b/test/utils/directionsRef.test.ts index 6dc2f6d6..397c0614 100644 --- a/test/utils/directionsRef.test.ts +++ b/test/utils/directionsRef.test.ts @@ -7,6 +7,7 @@ import { describe, it, expect, vi } from 'vitest'; import { DirectionsTool } from '../../src/tools/directions-tool/DirectionsTool.js'; import { directionsRequestFromRef } from '../../src/utils/directionsRef.js'; import { buildSelfFetchRef } from '../../src/utils/selfFetchRef.js'; +import type { HttpRequest } from '../../src/utils/types.js'; const coordinates = [ { longitude: -122.4194, latitude: 37.7749 }, @@ -35,7 +36,8 @@ function longRoute() { async function runDirections(input: Record) { const urls: string[] = []; - const httpRequest = vi.fn(async (url: string) => { + const httpRequest = vi.fn(async (input) => { + const url = String(input); urls.push(url); return new Response(JSON.stringify(longRoute()), { headers: { 'content-type': 'application/json' } diff --git a/test/utils/mapboxPublicToken.test.ts b/test/utils/mapboxPublicToken.test.ts index 770a4543..4ca977ca 100644 --- a/test/utils/mapboxPublicToken.test.ts +++ b/test/utils/mapboxPublicToken.test.ts @@ -14,6 +14,7 @@ import { resolveMapboxPublicToken, __resetMapboxPublicTokenCache } from '../../src/utils/mapboxPublicToken.js'; +import type { HttpRequest } from '../../src/utils/types.js'; const API_ENDPOINT = 'https://api.mapbox.com/'; const DEFAULT_PK = 'pk.eyJ1IjoidGVzdHVzZXIifQ.fake-public-token'; @@ -60,7 +61,8 @@ describe('resolveMapboxPublicToken', () => { }); it('resolves the default public token for a tk.* (OAuth) bearer', async () => { - const httpRequest = vi.fn(async (url: string) => { + const httpRequest = vi.fn(async (input) => { + const url = String(input); if (url.includes('tokens/v2/')) return makeOkJson(fakeTokenList) as Response; throw new Error(`Unexpected URL: ${url}`); @@ -79,7 +81,8 @@ describe('resolveMapboxPublicToken', () => { }); it('resolves the default public token for an sk.* bearer (regression)', async () => { - const httpRequest = vi.fn(async (url: string) => { + const httpRequest = vi.fn(async (input) => { + const url = String(input); if (url.includes('tokens/v2/')) return makeOkJson(fakeTokenList) as Response; throw new Error(`Unexpected URL: ${url}`); @@ -145,7 +148,8 @@ describe('resolveMapboxPublicToken', () => { }); it('caches the resolved token per-user and skips a second API call for the same user', async () => { - const httpRequest = vi.fn(async (url: string) => { + const httpRequest = vi.fn(async (input) => { + const url = String(input); if (url.includes('tokens/v2/')) return makeOkJson(fakeTokenList) as Response; throw new Error(`Unexpected URL: ${url}`); @@ -169,7 +173,8 @@ describe('resolveMapboxPublicToken', () => { it("never returns one user's cached token to a different user (cross-tenant isolation)", async () => { const otherUserPk = 'pk.eyJ1Ijoib3RoZXJ1c2VyIn0.other-users-token'; - const httpRequest = vi.fn(async (url: string) => { + const httpRequest = vi.fn(async (input) => { + const url = String(input); if (url.includes('tokens/v2/testuser')) return makeOkJson(fakeTokenList) as Response; if (url.includes('tokens/v2/otheruser')) diff --git a/test/utils/resultTestUtils.ts b/test/utils/resultTestUtils.ts new file mode 100644 index 00000000..2f94266d --- /dev/null +++ b/test/utils/resultTestUtils.ts @@ -0,0 +1,41 @@ +// Copyright (c) Mapbox, Inc. +// Licensed under the MIT License. + +import type { + CallToolResult, + ReadResourceResult +} from '@modelcontextprotocol/server'; + +/** + * A tool result's `structuredContent`, typed as the tool's output type (for + * example `structuredOf(result)`). The SDK types it as an + * open object, so tests otherwise can't reach the fields they check. + */ +export function structuredOf(result: CallToolResult): T { + return result.structuredContent as T; +} + +/** + * 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; +} diff --git a/tsconfig.test.json b/tsconfig.test.json index 64c3b90e..aeec02cf 100644 --- a/tsconfig.test.json +++ b/tsconfig.test.json @@ -1,9 +1,10 @@ -{ +{ "extends": "./tsconfig.base.json", "compilerOptions": { "skipLibCheck": true, + "noEmit": true, "lib": ["ESNext"], "resolveJsonModule": true }, "include": ["test", "src"] -} \ No newline at end of file +} From ba55ab5725aef6df4efce7a95379bf0989688635 Mon Sep 17 00:00:00 2001 From: Matthew Podwysocki Date: Wed, 21 Oct 2026 19:15:09 -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 a77088d9..812da20b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,7 +23,7 @@ ### Maintenance -- **Tests are typechecked in CI (#PR).** `tsconfig.test.json` covers `src/` and `test/`, but nothing ran it, so test type errors piled up: about 200 on main, plus about 250 more from SDK v2 typing `structuredContent` as an open object (#296). They're all fixed, and `npm run typecheck` now runs in CI before the tests. `tsconfig.test.json` also gets `noEmit`, since running it without that writes `.js` files next to the sources (see #297). +- **Tests are typechecked in CI (#298).** `tsconfig.test.json` covers `src/` and `test/`, but nothing ran it, so test type errors piled up: about 200 on main, plus about 250 more from SDK v2 typing `structuredContent` as an open object (#296). They're all fixed, and `npm run typecheck` now runs in CI before the tests. `tsconfig.test.json` also gets `noEmit`, since running it without that writes `.js` files next to the sources (see #297). - **New helpers** in `test/utils/resultTestUtils.ts`: `structuredOf(result)` reads structured content as the tool's own output type, so a test that checks a field the schema doesn't have fails to compile. `textOf` and `resourceTextOf` read text content and throw on the wrong content type. - **Tests that were wrong:** the tuple check in `input-schema-validation.test.ts` never looked inside arrays under zod 4 (`ZodArray._def.type` is now the string `'array'`), so a tuple nested in an array would have passed. It uses zod 4's accessors now and covers that case. `structured-content.test.ts` imported a file that never existed. `cleanResponseData.test.ts` used an old directions input shape and now builds it with `DirectionsInputSchema.parse`. A `|| Infinity` fallback in a SimplifyTool test meant one comparison could never fail.