From 4853cacbc1d24d1624acaae22ac791189876cfb2 Mon Sep 17 00:00:00 2001 From: jussi-sa Date: Wed, 22 Oct 2025 10:48:25 +0300 Subject: [PATCH] Revert "Merge pull request #33 from mapbox/output_schemas" This reverts commit 158a5ac72115542fa8fcb5d3223180db134254ae, reversing changes made to fbd9018a4b7761d7b5b7ffc942ee85c3bd7b461d. --- cspell.config.json | 16 - package-lock.json | 1038 +---------------- package.json | 17 +- src/index.ts | 3 - src/schemas/style.ts | 223 ---- src/tools/BaseTool.ts | 129 +- src/tools/MapboxApiBasedTool.ts | 179 +-- .../tool-naming-convention.test.ts.snap | 86 ++ .../bounding-box-tool/BoundariesData-cjs.cts | 3 - src/tools/bounding-box-tool/BoundariesData.ts | 3 - .../BoundingBoxTool.output.schema.ts | 18 - ...ut.schema.ts => BoundingBoxTool.schema.ts} | 3 - .../bounding-box-tool/BoundingBoxTool.ts | 70 +- .../CountryBoundingBoxTool.output.schema.ts | 20 - ...ma.ts => CountryBoundingBoxTool.schema.ts} | 3 - .../CountryBoundingBoxTool.ts | 39 +- .../CoordinateConversionTool.output.schema.ts | 16 - ....ts => CoordinateConversionTool.schema.ts} | 3 - .../CoordinateConversionTool.ts | 108 +- .../CreateStyleTool.input.schema.ts | 17 - .../CreateStyleTool.output.schema.ts | 30 - .../CreateStyleTool.schema.ts | 8 + .../create-style-tool/CreateStyleTool.ts | 74 +- .../CreateTokenTool.output.schema.ts | 31 - ...ut.schema.ts => CreateTokenTool.schema.ts} | 3 - .../create-token-tool/CreateTokenTool.ts | 99 +- ...ut.schema.ts => DeleteStyleTool.schema.ts} | 0 .../delete-style-tool/DeleteStyleTool.ts | 41 +- ...schema.ts => GeojsonPreviewTool.schema.ts} | 3 - .../GeojsonPreviewTool.ts | 39 +- ...ma.ts => GetMapboxDocSourceTool.schema.ts} | 3 - .../GetMapboxDocSourceTool.ts | 52 +- .../ListStylesTool.output.schema.ts | 50 - ...put.schema.ts => ListStylesTool.schema.ts} | 3 - src/tools/list-styles-tool/ListStylesTool.ts | 68 +- .../ListTokensTool.output.schema.ts | 43 - ...put.schema.ts => ListTokensTool.schema.ts} | 0 src/tools/list-tokens-tool/ListTokensTool.ts | 110 +- ...t.schema.ts => PreviewStyleTool.schema.ts} | 0 .../preview-style-tool/PreviewStyleTool.ts | 34 +- .../RetrieveStyleTool.output.schema.ts | 34 - ....schema.ts => RetrieveStyleTool.schema.ts} | 0 .../retrieve-style-tool/RetrieveStyleTool.ts | 61 +- ...t.schema.ts => StyleBuilderTool.schema.ts} | 3 - .../style-builder-tool/StyleBuilderTool.ts | 14 +- .../StyleComparisonTool.schema.ts | 3 - .../StyleComparisonTool.ts | 42 +- .../TilequeryTool.output.schema.ts | 81 -- ...nput.schema.ts => TilequeryTool.schema.ts} | 3 - src/tools/tilequery-tool/TilequeryTool.ts | 67 +- src/tools/toolRegistry.ts | 22 +- .../UpdateStyleTool.input.schema.ts | 25 - .../UpdateStyleTool.output.schema.ts | 29 - .../UpdateStyleTool.schema.ts | 12 + .../update-style-tool/UpdateStyleTool.ts | 67 +- .../{httpPipeline.ts => fetchRequest.ts} | 61 +- src/utils/jwtUtils.ts | 53 - src/utils/styleUtils.ts | 3 - src/utils/types.ts | 9 - src/utils/versionUtils-cjs.cts | 3 - src/utils/versionUtils.ts | 3 - test/tools/MapboxApiBasedTool.test.ts | 116 +- .../bounding-box-tool/BoundingBoxTool.test.ts | 51 +- .../CountryBoundingBoxTool.test.ts | 27 +- .../CoordinateConversionTool.test.ts | 5 +- .../create-style-tool/CreateStyleTool.test.ts | 44 +- .../create-token-tool/CreateTokenTool.test.ts | 98 +- .../delete-style-tool/DeleteStyleTool.test.ts | 59 +- .../GeojsonPreviewTool.test.ts | 5 +- .../GetMapboxDocSourceTool.test.ts | 47 +- .../list-styles-tool/ListStylesTool.test.ts | 182 +-- .../list-tokens-tool/ListTokensTool.test.ts | 165 ++- .../PreviewStyleTool.test.ts | 35 +- .../RetrieveStyleTool.test.ts | 79 +- .../StyleBuilderTool.test.ts | 228 ++-- .../StyleComparisonTool.test.ts | 32 +- .../tilequery-tool/TilequeryTool.test.ts | 9 +- test/tools/tool-naming-convention.test.ts | 16 +- .../update-style-tool/UpdateStyleTool.test.ts | 28 +- test/utils/fetchRequest.test.ts | 325 ++++++ ...pPipelineUtils.ts => fetchRequestUtils.ts} | 18 +- test/utils/httpPipeline.test.ts | 268 ----- test/utils/jwtUtils.test.ts | 79 -- 83 files changed, 1375 insertions(+), 3821 deletions(-) delete mode 100644 cspell.config.json delete mode 100644 src/schemas/style.ts create mode 100644 src/tools/__snapshots__/tool-naming-convention.test.ts.snap delete mode 100644 src/tools/bounding-box-tool/BoundingBoxTool.output.schema.ts rename src/tools/bounding-box-tool/{BoundingBoxTool.input.schema.ts => BoundingBoxTool.schema.ts} (91%) delete mode 100644 src/tools/bounding-box-tool/CountryBoundingBoxTool.output.schema.ts rename src/tools/bounding-box-tool/{CountryBoundingBoxTool.input.schema.ts => CountryBoundingBoxTool.schema.ts} (82%) delete mode 100644 src/tools/coordinate-conversion-tool/CoordinateConversionTool.output.schema.ts rename src/tools/coordinate-conversion-tool/{CoordinateConversionTool.input.schema.ts => CoordinateConversionTool.schema.ts} (90%) delete mode 100644 src/tools/create-style-tool/CreateStyleTool.input.schema.ts delete mode 100644 src/tools/create-style-tool/CreateStyleTool.output.schema.ts create mode 100644 src/tools/create-style-tool/CreateStyleTool.schema.ts delete mode 100644 src/tools/create-token-tool/CreateTokenTool.output.schema.ts rename src/tools/create-token-tool/{CreateTokenTool.input.schema.ts => CreateTokenTool.schema.ts} (93%) rename src/tools/delete-style-tool/{DeleteStyleTool.input.schema.ts => DeleteStyleTool.schema.ts} (100%) rename src/tools/geojson-preview-tool/{GeojsonPreviewTool.input.schema.ts => GeojsonPreviewTool.schema.ts} (87%) rename src/tools/get-mapbox-doc-source-tool/{GetMapboxDocSourceTool.input.schema.ts => GetMapboxDocSourceTool.schema.ts} (70%) delete mode 100644 src/tools/list-styles-tool/ListStylesTool.output.schema.ts rename src/tools/list-styles-tool/{ListStylesTool.input.schema.ts => ListStylesTool.schema.ts} (87%) delete mode 100644 src/tools/list-tokens-tool/ListTokensTool.output.schema.ts rename src/tools/list-tokens-tool/{ListTokensTool.input.schema.ts => ListTokensTool.schema.ts} (100%) rename src/tools/preview-style-tool/{PreviewStyleTool.input.schema.ts => PreviewStyleTool.schema.ts} (100%) delete mode 100644 src/tools/retrieve-style-tool/RetrieveStyleTool.output.schema.ts rename src/tools/retrieve-style-tool/{RetrieveStyleTool.input.schema.ts => RetrieveStyleTool.schema.ts} (100%) rename src/tools/style-builder-tool/{StyleBuilderTool.input.schema.ts => StyleBuilderTool.schema.ts} (99%) delete mode 100644 src/tools/tilequery-tool/TilequeryTool.output.schema.ts rename src/tools/tilequery-tool/{TilequeryTool.input.schema.ts => TilequeryTool.schema.ts} (95%) delete mode 100644 src/tools/update-style-tool/UpdateStyleTool.input.schema.ts delete mode 100644 src/tools/update-style-tool/UpdateStyleTool.output.schema.ts create mode 100644 src/tools/update-style-tool/UpdateStyleTool.schema.ts rename src/utils/{httpPipeline.ts => fetchRequest.ts} (69%) delete mode 100644 src/utils/jwtUtils.ts delete mode 100644 src/utils/types.ts create mode 100644 test/utils/fetchRequest.test.ts rename test/utils/{httpPipelineUtils.ts => fetchRequestUtils.ts} (62%) delete mode 100644 test/utils/httpPipeline.test.ts delete mode 100644 test/utils/jwtUtils.test.ts diff --git a/cspell.config.json b/cspell.config.json deleted file mode 100644 index e8f6d4a..0000000 --- a/cspell.config.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "version": "0.2", - "language": "en", - "words": [ - "bbox", - "denoise", - "isochrone", - "mapbox", - "mmss", - "tilequery" - ], - "ignorePaths": [ - "node_modules", - "dist" - ] -} diff --git a/package-lock.json b/package-lock.json index 5f2a70d..1ace5b1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,6 @@ "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", "@vitest/coverage-istanbul": "^3.2.4", - "cspell": "^9.2.1", "eslint": "^9.0.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-n": "^17.21.3", @@ -278,594 +277,6 @@ "node": ">=6.9.0" } }, - "node_modules/@cspell/cspell-bundled-dicts": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/@cspell/cspell-bundled-dicts/-/cspell-bundled-dicts-9.2.1.tgz", - "integrity": "sha512-85gHoZh3rgZ/EqrHIr1/I4OLO53fWNp6JZCqCdgaT7e3sMDaOOG6HoSxCvOnVspXNIf/1ZbfTCDMx9x79Xq0AQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@cspell/dict-ada": "^4.1.1", - "@cspell/dict-al": "^1.1.1", - "@cspell/dict-aws": "^4.0.15", - "@cspell/dict-bash": "^4.2.1", - "@cspell/dict-companies": "^3.2.5", - "@cspell/dict-cpp": "^6.0.12", - "@cspell/dict-cryptocurrencies": "^5.0.5", - "@cspell/dict-csharp": "^4.0.7", - "@cspell/dict-css": "^4.0.18", - "@cspell/dict-dart": "^2.3.1", - "@cspell/dict-data-science": "^2.0.9", - "@cspell/dict-django": "^4.1.5", - "@cspell/dict-docker": "^1.1.16", - "@cspell/dict-dotnet": "^5.0.10", - "@cspell/dict-elixir": "^4.0.8", - "@cspell/dict-en_us": "^4.4.18", - "@cspell/dict-en-common-misspellings": "^2.1.5", - "@cspell/dict-en-gb-mit": "^3.1.8", - "@cspell/dict-filetypes": "^3.0.13", - "@cspell/dict-flutter": "^1.1.1", - "@cspell/dict-fonts": "^4.0.5", - "@cspell/dict-fsharp": "^1.1.1", - "@cspell/dict-fullstack": "^3.2.7", - "@cspell/dict-gaming-terms": "^1.1.2", - "@cspell/dict-git": "^3.0.7", - "@cspell/dict-golang": "^6.0.23", - "@cspell/dict-google": "^1.0.9", - "@cspell/dict-haskell": "^4.0.6", - "@cspell/dict-html": "^4.0.12", - "@cspell/dict-html-symbol-entities": "^4.0.4", - "@cspell/dict-java": "^5.0.12", - "@cspell/dict-julia": "^1.1.1", - "@cspell/dict-k8s": "^1.0.12", - "@cspell/dict-kotlin": "^1.1.1", - "@cspell/dict-latex": "^4.0.4", - "@cspell/dict-lorem-ipsum": "^4.0.5", - "@cspell/dict-lua": "^4.0.8", - "@cspell/dict-makefile": "^1.0.5", - "@cspell/dict-markdown": "^2.0.12", - "@cspell/dict-monkeyc": "^1.0.11", - "@cspell/dict-node": "^5.0.8", - "@cspell/dict-npm": "^5.2.15", - "@cspell/dict-php": "^4.0.15", - "@cspell/dict-powershell": "^5.0.15", - "@cspell/dict-public-licenses": "^2.0.15", - "@cspell/dict-python": "^4.2.19", - "@cspell/dict-r": "^2.1.1", - "@cspell/dict-ruby": "^5.0.9", - "@cspell/dict-rust": "^4.0.12", - "@cspell/dict-scala": "^5.0.8", - "@cspell/dict-shell": "^1.1.1", - "@cspell/dict-software-terms": "^5.1.7", - "@cspell/dict-sql": "^2.2.1", - "@cspell/dict-svelte": "^1.0.7", - "@cspell/dict-swift": "^2.0.6", - "@cspell/dict-terraform": "^1.1.3", - "@cspell/dict-typescript": "^3.2.3", - "@cspell/dict-vue": "^3.0.5" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/@cspell/cspell-json-reporter": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/@cspell/cspell-json-reporter/-/cspell-json-reporter-9.2.1.tgz", - "integrity": "sha512-LiiIWzLP9h2etKn0ap6g2+HrgOGcFEF/hp5D8ytmSL5sMxDcV13RrmJCEMTh1axGyW0SjQEFjPnYzNpCL1JjGA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@cspell/cspell-types": "9.2.1" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/@cspell/cspell-pipe": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/@cspell/cspell-pipe/-/cspell-pipe-9.2.1.tgz", - "integrity": "sha512-2N1H63If5cezLqKToY/YSXon4m4REg/CVTFZr040wlHRbbQMh5EF3c7tEC/ue3iKAQR4sm52ihfqo1n4X6kz+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20" - } - }, - "node_modules/@cspell/cspell-resolver": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/@cspell/cspell-resolver/-/cspell-resolver-9.2.1.tgz", - "integrity": "sha512-fRPQ6GWU5eyh8LN1TZblc7t24TlGhJprdjJkfZ+HjQo+6ivdeBPT7pC7pew6vuMBQPS1oHBR36hE0ZnJqqkCeg==", - "dev": true, - "license": "MIT", - "dependencies": { - "global-directory": "^4.0.1" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/@cspell/cspell-service-bus": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/@cspell/cspell-service-bus/-/cspell-service-bus-9.2.1.tgz", - "integrity": "sha512-k4M6bqdvWbcGSbcfLD7Lf4coZVObsISDW+sm/VaWp9aZ7/uwiz1IuGUxL9WO4JIdr9CFEf7Ivmvd2txZpVOCIA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20" - } - }, - "node_modules/@cspell/cspell-types": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/@cspell/cspell-types/-/cspell-types-9.2.1.tgz", - "integrity": "sha512-FQHgQYdTHkcpxT0u1ddLIg5Cc5ePVDcLg9+b5Wgaubmc5I0tLotgYj8c/mvStWuKsuZIs6sUopjJrE91wk6Onw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20" - } - }, - "node_modules/@cspell/dict-ada": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@cspell/dict-ada/-/dict-ada-4.1.1.tgz", - "integrity": "sha512-E+0YW9RhZod/9Qy2gxfNZiHJjCYFlCdI69br1eviQQWB8yOTJX0JHXLs79kOYhSW0kINPVUdvddEBe6Lu6CjGQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-al": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@cspell/dict-al/-/dict-al-1.1.1.tgz", - "integrity": "sha512-sD8GCaZetgQL4+MaJLXqbzWcRjfKVp8x+px3HuCaaiATAAtvjwUQ5/Iubiqwfd1boIh2Y1/3EgM3TLQ7Q8e0wQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-aws": { - "version": "4.0.15", - "resolved": "https://registry.npmjs.org/@cspell/dict-aws/-/dict-aws-4.0.15.tgz", - "integrity": "sha512-aPY7VVR5Os4rz36EaqXBAEy14wR4Rqv+leCJ2Ug/Gd0IglJpM30LalF3e2eJChnjje3vWoEC0Rz3+e5gpZG+Kg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-bash": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@cspell/dict-bash/-/dict-bash-4.2.1.tgz", - "integrity": "sha512-SBnzfAyEAZLI9KFS7DUG6Xc1vDFuLllY3jz0WHvmxe8/4xV3ufFE3fGxalTikc1VVeZgZmxYiABw4iGxVldYEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@cspell/dict-shell": "1.1.1" - } - }, - "node_modules/@cspell/dict-companies": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/@cspell/dict-companies/-/dict-companies-3.2.6.tgz", - "integrity": "sha512-cVWBk4DSUOthCsgOsoB+5L5F1Wk8lWGHnw5de75YCKSjOEV8/6kskwwDrPTIHkoGVzpIzIIQ/OdXhYwa2G+16A==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-cpp": { - "version": "6.0.12", - "resolved": "https://registry.npmjs.org/@cspell/dict-cpp/-/dict-cpp-6.0.12.tgz", - "integrity": "sha512-N4NsCTttVpMqQEYbf0VQwCj6np+pJESov0WieCN7R/0aByz4+MXEiDieWWisaiVi8LbKzs1mEj4ZTw5K/6O2UQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-cryptocurrencies": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/@cspell/dict-cryptocurrencies/-/dict-cryptocurrencies-5.0.5.tgz", - "integrity": "sha512-R68hYYF/rtlE6T/dsObStzN5QZw+0aQBinAXuWCVqwdS7YZo0X33vGMfChkHaiCo3Z2+bkegqHlqxZF4TD3rUA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-csharp": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/@cspell/dict-csharp/-/dict-csharp-4.0.7.tgz", - "integrity": "sha512-H16Hpu8O/1/lgijFt2lOk4/nnldFtQ4t8QHbyqphqZZVE5aS4J/zD/WvduqnLY21aKhZS6jo/xF5PX9jyqPKUA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-css": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@cspell/dict-css/-/dict-css-4.0.18.tgz", - "integrity": "sha512-EF77RqROHL+4LhMGW5NTeKqfUd/e4OOv6EDFQ/UQQiFyWuqkEKyEz0NDILxOFxWUEVdjT2GQ2cC7t12B6pESwg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-dart": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@cspell/dict-dart/-/dict-dart-2.3.1.tgz", - "integrity": "sha512-xoiGnULEcWdodXI6EwVyqpZmpOoh8RA2Xk9BNdR7DLamV/QMvEYn8KJ7NlRiTSauJKPNkHHQ5EVHRM6sTS7jdg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-data-science": { - "version": "2.0.10", - "resolved": "https://registry.npmjs.org/@cspell/dict-data-science/-/dict-data-science-2.0.10.tgz", - "integrity": "sha512-vZSsz7845ugW6mY65966Ki2bMS/ZnAZoTVvpuXQ07a2rYxJhUC+6WuBMD80hFLlKwjC5T/5Llv4F/VlB00swpw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-django": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@cspell/dict-django/-/dict-django-4.1.5.tgz", - "integrity": "sha512-AvTWu99doU3T8ifoMYOMLW2CXKvyKLukPh1auOPwFGHzueWYvBBN+OxF8wF7XwjTBMMeRleVdLh3aWCDEX/ZWg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-docker": { - "version": "1.1.16", - "resolved": "https://registry.npmjs.org/@cspell/dict-docker/-/dict-docker-1.1.16.tgz", - "integrity": "sha512-UiVQ5RmCg6j0qGIxrBnai3pIB+aYKL3zaJGvXk1O/ertTKJif9RZikKXCEgqhaCYMweM4fuLqWSVmw3hU164Iw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-dotnet": { - "version": "5.0.10", - "resolved": "https://registry.npmjs.org/@cspell/dict-dotnet/-/dict-dotnet-5.0.10.tgz", - "integrity": "sha512-ooar8BP/RBNP1gzYfJPStKEmpWy4uv/7JCq6FOnJLeD1yyfG3d/LFMVMwiJo+XWz025cxtkM3wuaikBWzCqkmg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-elixir": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@cspell/dict-elixir/-/dict-elixir-4.0.8.tgz", - "integrity": "sha512-CyfphrbMyl4Ms55Vzuj+mNmd693HjBFr9hvU+B2YbFEZprE5AG+EXLYTMRWrXbpds4AuZcvN3deM2XVB80BN/Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-en_us": { - "version": "4.4.20", - "resolved": "https://registry.npmjs.org/@cspell/dict-en_us/-/dict-en_us-4.4.20.tgz", - "integrity": "sha512-acAlX967bkrLwRhSJ8KGBCBUITMOe8+smwsShjei431vTB6tU5ZID6XDxR9hH/kDxfdiRTXAE8vkT3WJAHnc1Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-en-common-misspellings": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@cspell/dict-en-common-misspellings/-/dict-en-common-misspellings-2.1.6.tgz", - "integrity": "sha512-xV9yryOqZizbSqxRS7kSVRrxVEyWHUqwdY56IuT7eAWGyTCJNmitXzXa4p+AnEbhL+AB2WLynGVSbNoUC3ceFA==", - "dev": true, - "license": "CC BY-SA 4.0" - }, - "node_modules/@cspell/dict-en-gb-mit": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/@cspell/dict-en-gb-mit/-/dict-en-gb-mit-3.1.10.tgz", - "integrity": "sha512-oFandL5N4B55wmOd0hOAoyaiUZBkClQ1FPCkcAY/HMuq6zeCQE/oEK9lLGDmnzLGgWnTT7wd0KOSYUPTxWQaNQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-filetypes": { - "version": "3.0.14", - "resolved": "https://registry.npmjs.org/@cspell/dict-filetypes/-/dict-filetypes-3.0.14.tgz", - "integrity": "sha512-KSXaSMYYNMLLdHEnju1DyRRH3eQWPRYRnOXpuHUdOh2jC44VgQoxyMU7oB3NAhDhZKBPCihabzECsAGFbdKfEA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-flutter": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@cspell/dict-flutter/-/dict-flutter-1.1.1.tgz", - "integrity": "sha512-UlOzRcH2tNbFhZmHJN48Za/2/MEdRHl2BMkCWZBYs+30b91mWvBfzaN4IJQU7dUZtowKayVIF9FzvLZtZokc5A==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-fonts": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@cspell/dict-fonts/-/dict-fonts-4.0.5.tgz", - "integrity": "sha512-BbpkX10DUX/xzHs6lb7yzDf/LPjwYIBJHJlUXSBXDtK/1HaeS+Wqol4Mlm2+NAgZ7ikIE5DQMViTgBUY3ezNoQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-fsharp": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@cspell/dict-fsharp/-/dict-fsharp-1.1.1.tgz", - "integrity": "sha512-imhs0u87wEA4/cYjgzS0tAyaJpwG7vwtC8UyMFbwpmtw+/bgss+osNfyqhYRyS/ehVCWL17Ewx2UPkexjKyaBA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-fullstack": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@cspell/dict-fullstack/-/dict-fullstack-3.2.7.tgz", - "integrity": "sha512-IxEk2YAwAJKYCUEgEeOg3QvTL4XLlyArJElFuMQevU1dPgHgzWElFevN5lsTFnvMFA1riYsVinqJJX0BanCFEg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-gaming-terms": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@cspell/dict-gaming-terms/-/dict-gaming-terms-1.1.2.tgz", - "integrity": "sha512-9XnOvaoTBscq0xuD6KTEIkk9hhdfBkkvJAIsvw3JMcnp1214OCGW8+kako5RqQ2vTZR3Tnf3pc57o7VgkM0q1Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-git": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@cspell/dict-git/-/dict-git-3.0.7.tgz", - "integrity": "sha512-odOwVKgfxCQfiSb+nblQZc4ErXmnWEnv8XwkaI4sNJ7cNmojnvogYVeMqkXPjvfrgEcizEEA4URRD2Ms5PDk1w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-golang": { - "version": "6.0.23", - "resolved": "https://registry.npmjs.org/@cspell/dict-golang/-/dict-golang-6.0.23.tgz", - "integrity": "sha512-oXqUh/9dDwcmVlfUF5bn3fYFqbUzC46lXFQmi5emB0vYsyQXdNWsqi6/yH3uE7bdRE21nP7Yo0mR1jjFNyLamg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-google": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@cspell/dict-google/-/dict-google-1.0.9.tgz", - "integrity": "sha512-biL65POqialY0i4g6crj7pR6JnBkbsPovB2WDYkj3H4TuC/QXv7Pu5pdPxeUJA6TSCHI7T5twsO4VSVyRxD9CA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-haskell": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@cspell/dict-haskell/-/dict-haskell-4.0.6.tgz", - "integrity": "sha512-ib8SA5qgftExpYNjWhpYIgvDsZ/0wvKKxSP+kuSkkak520iPvTJumEpIE+qPcmJQo4NzdKMN8nEfaeci4OcFAQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-html": { - "version": "4.0.12", - "resolved": "https://registry.npmjs.org/@cspell/dict-html/-/dict-html-4.0.12.tgz", - "integrity": "sha512-JFffQ1dDVEyJq6tCDWv0r/RqkdSnV43P2F/3jJ9rwLgdsOIXwQbXrz6QDlvQLVvNSnORH9KjDtenFTGDyzfCaA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-html-symbol-entities": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@cspell/dict-html-symbol-entities/-/dict-html-symbol-entities-4.0.4.tgz", - "integrity": "sha512-afea+0rGPDeOV9gdO06UW183Qg6wRhWVkgCFwiO3bDupAoyXRuvupbb5nUyqSTsLXIKL8u8uXQlJ9pkz07oVXw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-java": { - "version": "5.0.12", - "resolved": "https://registry.npmjs.org/@cspell/dict-java/-/dict-java-5.0.12.tgz", - "integrity": "sha512-qPSNhTcl7LGJ5Qp6VN71H8zqvRQK04S08T67knMq9hTA8U7G1sTKzLmBaDOFhq17vNX/+rT+rbRYp+B5Nwza1A==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-julia": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@cspell/dict-julia/-/dict-julia-1.1.1.tgz", - "integrity": "sha512-WylJR9TQ2cgwd5BWEOfdO3zvDB+L7kYFm0I9u0s9jKHWQ6yKmfKeMjU9oXxTBxIufhCXm92SKwwVNAC7gjv+yA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-k8s": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/@cspell/dict-k8s/-/dict-k8s-1.0.12.tgz", - "integrity": "sha512-2LcllTWgaTfYC7DmkMPOn9GsBWsA4DZdlun4po8s2ysTP7CPEnZc1ZfK6pZ2eI4TsZemlUQQ+NZxMe9/QutQxg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-kotlin": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@cspell/dict-kotlin/-/dict-kotlin-1.1.1.tgz", - "integrity": "sha512-J3NzzfgmxRvEeOe3qUXnSJQCd38i/dpF9/t3quuWh6gXM+krsAXP75dY1CzDmS8mrJAlBdVBeAW5eAZTD8g86Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-latex": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@cspell/dict-latex/-/dict-latex-4.0.4.tgz", - "integrity": "sha512-YdTQhnTINEEm/LZgTzr9Voz4mzdOXH7YX+bSFs3hnkUHCUUtX/mhKgf1CFvZ0YNM2afjhQcmLaR9bDQVyYBvpA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-lorem-ipsum": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@cspell/dict-lorem-ipsum/-/dict-lorem-ipsum-4.0.5.tgz", - "integrity": "sha512-9a4TJYRcPWPBKkQAJ/whCu4uCAEgv/O2xAaZEI0n4y1/l18Yyx8pBKoIX5QuVXjjmKEkK7hi5SxyIsH7pFEK9Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-lua": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@cspell/dict-lua/-/dict-lua-4.0.8.tgz", - "integrity": "sha512-N4PkgNDMu9JVsRu7JBS/3E/dvfItRgk9w5ga2dKq+JupP2Y3lojNaAVFhXISh4Y0a6qXDn2clA6nvnavQ/jjLA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-makefile": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@cspell/dict-makefile/-/dict-makefile-1.0.5.tgz", - "integrity": "sha512-4vrVt7bGiK8Rx98tfRbYo42Xo2IstJkAF4tLLDMNQLkQ86msDlYSKG1ZCk8Abg+EdNcFAjNhXIiNO+w4KflGAQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-markdown": { - "version": "2.0.12", - "resolved": "https://registry.npmjs.org/@cspell/dict-markdown/-/dict-markdown-2.0.12.tgz", - "integrity": "sha512-ufwoliPijAgWkD/ivAMC+A9QD895xKiJRF/fwwknQb7kt7NozTLKFAOBtXGPJAB4UjhGBpYEJVo2elQ0FCAH9A==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@cspell/dict-css": "^4.0.18", - "@cspell/dict-html": "^4.0.12", - "@cspell/dict-html-symbol-entities": "^4.0.4", - "@cspell/dict-typescript": "^3.2.3" - } - }, - "node_modules/@cspell/dict-monkeyc": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@cspell/dict-monkeyc/-/dict-monkeyc-1.0.11.tgz", - "integrity": "sha512-7Q1Ncu0urALI6dPTrEbSTd//UK0qjRBeaxhnm8uY5fgYNFYAG+u4gtnTIo59S6Bw5P++4H3DiIDYoQdY/lha8w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-node": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/@cspell/dict-node/-/dict-node-5.0.8.tgz", - "integrity": "sha512-AirZcN2i84ynev3p2/1NCPEhnNsHKMz9zciTngGoqpdItUb2bDt1nJBjwlsrFI78GZRph/VaqTVFwYikmncpXg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-npm": { - "version": "5.2.18", - "resolved": "https://registry.npmjs.org/@cspell/dict-npm/-/dict-npm-5.2.18.tgz", - "integrity": "sha512-uJV1T7y9ifFysO22XmxjU7y95c+02lfCZHNsTYHw2KOL6tLjc3XK/i0xt9iGLkPpcxwNJSCdu13UpjXZGqce/Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-php": { - "version": "4.0.15", - "resolved": "https://registry.npmjs.org/@cspell/dict-php/-/dict-php-4.0.15.tgz", - "integrity": "sha512-iepGB2gtToMWSTvybesn4/lUp4LwXcEm0s8vasJLP76WWVkq1zYjmeS+WAIzNgsuURyZ/9mGqhS0CWMuo74ODw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-powershell": { - "version": "5.0.15", - "resolved": "https://registry.npmjs.org/@cspell/dict-powershell/-/dict-powershell-5.0.15.tgz", - "integrity": "sha512-l4S5PAcvCFcVDMJShrYD0X6Huv9dcsQPlsVsBGbH38wvuN7gS7+GxZFAjTNxDmTY1wrNi1cCatSg6Pu2BW4rgg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-public-licenses": { - "version": "2.0.15", - "resolved": "https://registry.npmjs.org/@cspell/dict-public-licenses/-/dict-public-licenses-2.0.15.tgz", - "integrity": "sha512-cJEOs901H13Pfy0fl4dCD1U+xpWIMaEPq8MeYU83FfDZvellAuSo4GqWCripfIqlhns/L6+UZEIJSOZnjgy7Wg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-python": { - "version": "4.2.20", - "resolved": "https://registry.npmjs.org/@cspell/dict-python/-/dict-python-4.2.20.tgz", - "integrity": "sha512-c1wbfb3MDMSY4UTNdGnA18NkrcX6cMlYER0HSpGYh2jLK43gS1QL3j2B49qgnRYfcLUp4xgeA05vzCQsjGbwuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@cspell/dict-data-science": "^2.0.10" - } - }, - "node_modules/@cspell/dict-r": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@cspell/dict-r/-/dict-r-2.1.1.tgz", - "integrity": "sha512-71Ka+yKfG4ZHEMEmDxc6+blFkeTTvgKbKAbwiwQAuKl3zpqs1Y0vUtwW2N4b3LgmSPhV3ODVY0y4m5ofqDuKMw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-ruby": { - "version": "5.0.9", - "resolved": "https://registry.npmjs.org/@cspell/dict-ruby/-/dict-ruby-5.0.9.tgz", - "integrity": "sha512-H2vMcERMcANvQshAdrVx0XoWaNX8zmmiQN11dZZTQAZaNJ0xatdJoSqY8C8uhEMW89bfgpN+NQgGuDXW2vmXEw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-rust": { - "version": "4.0.12", - "resolved": "https://registry.npmjs.org/@cspell/dict-rust/-/dict-rust-4.0.12.tgz", - "integrity": "sha512-z2QiH+q9UlNhobBJArvILRxV8Jz0pKIK7gqu4TgmEYyjiu1TvnGZ1tbYHeu9w3I/wOP6UMDoCBTty5AlYfW0mw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-scala": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/@cspell/dict-scala/-/dict-scala-5.0.8.tgz", - "integrity": "sha512-YdftVmumv8IZq9zu1gn2U7A4bfM2yj9Vaupydotyjuc+EEZZSqAafTpvW/jKLWji2TgybM1L2IhmV0s/Iv9BTw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-shell": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@cspell/dict-shell/-/dict-shell-1.1.1.tgz", - "integrity": "sha512-T37oYxE7OV1x/1D4/13Y8JZGa1QgDCXV7AVt3HLXjn0Fe3TaNDvf5sU0fGnXKmBPqFFrHdpD3uutAQb1dlp15g==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-software-terms": { - "version": "5.1.9", - "resolved": "https://registry.npmjs.org/@cspell/dict-software-terms/-/dict-software-terms-5.1.9.tgz", - "integrity": "sha512-lpiSpS1iTF2n8barqVkPmhe5qXs5291IqcDUPr5ttFRxPMZ7pgrMUdvcdNUdkajymjDOyWfUNhdYXW7JndThZw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-sql": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@cspell/dict-sql/-/dict-sql-2.2.1.tgz", - "integrity": "sha512-qDHF8MpAYCf4pWU8NKbnVGzkoxMNrFqBHyG/dgrlic5EQiKANCLELYtGlX5auIMDLmTf1inA0eNtv74tyRJ/vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-svelte": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@cspell/dict-svelte/-/dict-svelte-1.0.7.tgz", - "integrity": "sha512-hGZsGqP0WdzKkdpeVLBivRuSNzOTvN036EBmpOwxH+FTY2DuUH7ecW+cSaMwOgmq5JFSdTcbTNFlNC8HN8lhaQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-swift": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@cspell/dict-swift/-/dict-swift-2.0.6.tgz", - "integrity": "sha512-PnpNbrIbex2aqU1kMgwEKvCzgbkHtj3dlFLPMqW1vSniop7YxaDTtvTUO4zA++ugYAEL+UK8vYrBwDPTjjvSnA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-terraform": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@cspell/dict-terraform/-/dict-terraform-1.1.3.tgz", - "integrity": "sha512-gr6wxCydwSFyyBKhBA2xkENXtVFToheqYYGFvlMZXWjviynXmh+NK/JTvTCk/VHk3+lzbO9EEQKee6VjrAUSbA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-typescript": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/@cspell/dict-typescript/-/dict-typescript-3.2.3.tgz", - "integrity": "sha512-zXh1wYsNljQZfWWdSPYwQhpwiuW0KPW1dSd8idjMRvSD0aSvWWHoWlrMsmZeRl4qM4QCEAjua8+cjflm41cQBg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dict-vue": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@cspell/dict-vue/-/dict-vue-3.0.5.tgz", - "integrity": "sha512-Mqutb8jbM+kIcywuPQCCaK5qQHTdaByoEO2J9LKFy3sqAdiBogNkrplqUK0HyyRFgCfbJUgjz3N85iCMcWH0JA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspell/dynamic-import": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/@cspell/dynamic-import/-/dynamic-import-9.2.1.tgz", - "integrity": "sha512-izYQbk7ck0ffNA1gf7Gi3PkUEjj+crbYeyNK1hxHx5A+GuR416ozs0aEyp995KI2v9HZlXscOj3SC3wrWzHZeA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@cspell/url": "9.2.1", - "import-meta-resolve": "^4.2.0" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/@cspell/filetypes": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/@cspell/filetypes/-/filetypes-9.2.1.tgz", - "integrity": "sha512-Dy1y1pQ+7hi2gPs+jERczVkACtYbUHcLodXDrzpipoxgOtVxMcyZuo+84WYHImfu0gtM0wU2uLObaVgMSTnytw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20" - } - }, - "node_modules/@cspell/strong-weak-map": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/@cspell/strong-weak-map/-/strong-weak-map-9.2.1.tgz", - "integrity": "sha512-1HsQWZexvJSjDocVnbeAWjjgqWE/0op/txxzDPvDqI2sE6pY0oO4Cinj2I8z+IP+m6/E6yjPxdb23ydbQbPpJQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20" - } - }, - "node_modules/@cspell/url": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/@cspell/url/-/url-9.2.1.tgz", - "integrity": "sha512-9EHCoGKtisPNsEdBQ28tKxKeBmiVS3D4j+AN8Yjr+Dmtu+YACKGWiMOddNZG2VejQNIdFx7FwzU00BGX68ELhA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20" - } - }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.9", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz", @@ -2740,15 +2151,8 @@ "dev": true, "license": "MIT", "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/array-timsort": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/array-timsort/-/array-timsort-1.0.3.tgz", - "integrity": "sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==", - "dev": true, - "license": "MIT" + "node": ">=0.10.0" + } }, "node_modules/assertion-error": { "version": "2.0.1", @@ -3018,35 +2422,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/chalk-template": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/chalk-template/-/chalk-template-1.1.2.tgz", - "integrity": "sha512-2bxTP2yUH7AJj/VAXfcA+4IcWGdQ87HwBANLt5XxGTeomo8yG0y95N1um9i5StvhT/Bl0/2cARA5v1PpPXUxUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^5.2.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/chalk/chalk-template?sponsor=1" - } - }, - "node_modules/chalk-template/node_modules/chalk": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, "node_modules/change-case": { "version": "4.1.2", "dev": true, @@ -3144,46 +2519,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/clear-module": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/clear-module/-/clear-module-4.1.2.tgz", - "integrity": "sha512-LWAxzHqdHsAZlPlEyJ2Poz6AIs384mPeqLVCru2p0BrP9G/kVGuhNyZYClLO6cXlnuJjzC8xtsJIuMjKqLXoAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^2.0.0", - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/clear-module/node_modules/parent-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-2.0.0.tgz", - "integrity": "sha512-uo0Z9JJeWzv8BG+tRcapBKNJ0dro9cLyczGzulS6EfeyAdeC9sbojtW6XwvYxJkEne9En+J2XEl4zyglVeIwFg==", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/clear-module/node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/cli-cursor": { "version": "5.0.0", "dev": true, @@ -3315,21 +2650,6 @@ "node": ">=20" } }, - "node_modules/comment-json": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/comment-json/-/comment-json-4.4.1.tgz", - "integrity": "sha512-r1To31BQD5060QdkC+Iheai7gHwoSZobzunqkf2/kQ6xIAfJyrKNAFUwdKvkK7Qgu7pVTKQEa7ok7Ed3ycAJgg==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-timsort": "^1.0.3", - "core-util-is": "^1.0.3", - "esprima": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/concat-map": { "version": "0.0.1", "dev": true, @@ -3381,13 +2701,6 @@ "node": ">=6.6.0" } }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true, - "license": "MIT" - }, "node_modules/cors": { "version": "2.8.5", "license": "MIT", @@ -3411,225 +2724,6 @@ "node": ">= 8" } }, - "node_modules/cspell": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/cspell/-/cspell-9.2.1.tgz", - "integrity": "sha512-PoKGKE9Tl87Sn/jwO4jvH7nTqe5Xrsz2DeJT5CkulY7SoL2fmsAqfbImQOFS2S0s36qD98t6VO+Ig2elEEcHew==", - "dev": true, - "license": "MIT", - "dependencies": { - "@cspell/cspell-json-reporter": "9.2.1", - "@cspell/cspell-pipe": "9.2.1", - "@cspell/cspell-types": "9.2.1", - "@cspell/dynamic-import": "9.2.1", - "@cspell/url": "9.2.1", - "chalk": "^5.6.0", - "chalk-template": "^1.1.0", - "commander": "^14.0.0", - "cspell-config-lib": "9.2.1", - "cspell-dictionary": "9.2.1", - "cspell-gitignore": "9.2.1", - "cspell-glob": "9.2.1", - "cspell-io": "9.2.1", - "cspell-lib": "9.2.1", - "fast-json-stable-stringify": "^2.1.0", - "flatted": "^3.3.3", - "semver": "^7.7.2", - "tinyglobby": "^0.2.14" - }, - "bin": { - "cspell": "bin.mjs", - "cspell-esm": "bin.mjs" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/streetsidesoftware/cspell?sponsor=1" - } - }, - "node_modules/cspell-config-lib": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/cspell-config-lib/-/cspell-config-lib-9.2.1.tgz", - "integrity": "sha512-qqhaWW+0Ilc7493lXAlXjziCyeEmQbmPMc1XSJw2EWZmzb+hDvLdFGHoX18QU67yzBtu5hgQsJDEDZKvVDTsRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@cspell/cspell-types": "9.2.1", - "comment-json": "^4.2.5", - "smol-toml": "^1.4.2", - "yaml": "^2.8.1" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/cspell-dictionary": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/cspell-dictionary/-/cspell-dictionary-9.2.1.tgz", - "integrity": "sha512-0hQVFySPsoJ0fONmDPwCWGSG6SGj4ERolWdx4t42fzg5zMs+VYGXpQW4BJneQ5Tfxy98Wx8kPhmh/9E8uYzLTw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@cspell/cspell-pipe": "9.2.1", - "@cspell/cspell-types": "9.2.1", - "cspell-trie-lib": "9.2.1", - "fast-equals": "^5.2.2" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/cspell-gitignore": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/cspell-gitignore/-/cspell-gitignore-9.2.1.tgz", - "integrity": "sha512-WPnDh03gXZoSqVyXq4L7t9ljx6lTDvkiSRUudb125egEK5e9s04csrQpLI3Yxcnc1wQA2nzDr5rX9XQVvCHf7g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@cspell/url": "9.2.1", - "cspell-glob": "9.2.1", - "cspell-io": "9.2.1" - }, - "bin": { - "cspell-gitignore": "bin.mjs" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/cspell-glob": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/cspell-glob/-/cspell-glob-9.2.1.tgz", - "integrity": "sha512-CrT/6ld3rXhB36yWFjrx1SrMQzwDrGOLr+wYEnrWI719/LTYWWCiMFW7H+qhsJDTsR+ku8+OAmfRNBDXvh9mnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@cspell/url": "9.2.1", - "picomatch": "^4.0.3" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/cspell-glob/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/cspell-grammar": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/cspell-grammar/-/cspell-grammar-9.2.1.tgz", - "integrity": "sha512-10RGFG7ZTQPdwyW2vJyfmC1t8813y8QYRlVZ8jRHWzer9NV8QWrGnL83F+gTPXiKR/lqiW8WHmFlXR4/YMV+JQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@cspell/cspell-pipe": "9.2.1", - "@cspell/cspell-types": "9.2.1" - }, - "bin": { - "cspell-grammar": "bin.mjs" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/cspell-io": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/cspell-io/-/cspell-io-9.2.1.tgz", - "integrity": "sha512-v9uWXtRzB+RF/Mzg5qMzpb8/yt+1bwtTt2rZftkLDLrx5ybVvy6rhRQK05gFWHmWVtWEe0P/pIxaG2Vz92C8Ag==", - "dev": true, - "license": "MIT", - "dependencies": { - "@cspell/cspell-service-bus": "9.2.1", - "@cspell/url": "9.2.1" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/cspell-lib": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/cspell-lib/-/cspell-lib-9.2.1.tgz", - "integrity": "sha512-KeB6NHcO0g1knWa7sIuDippC3gian0rC48cvO0B0B0QwhOxNxWVp8cSmkycXjk4ijBZNa++IwFjeK/iEqMdahQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@cspell/cspell-bundled-dicts": "9.2.1", - "@cspell/cspell-pipe": "9.2.1", - "@cspell/cspell-resolver": "9.2.1", - "@cspell/cspell-types": "9.2.1", - "@cspell/dynamic-import": "9.2.1", - "@cspell/filetypes": "9.2.1", - "@cspell/strong-weak-map": "9.2.1", - "@cspell/url": "9.2.1", - "clear-module": "^4.1.2", - "comment-json": "^4.2.5", - "cspell-config-lib": "9.2.1", - "cspell-dictionary": "9.2.1", - "cspell-glob": "9.2.1", - "cspell-grammar": "9.2.1", - "cspell-io": "9.2.1", - "cspell-trie-lib": "9.2.1", - "env-paths": "^3.0.0", - "fast-equals": "^5.2.2", - "gensequence": "^7.0.0", - "import-fresh": "^3.3.1", - "resolve-from": "^5.0.0", - "vscode-languageserver-textdocument": "^1.0.12", - "vscode-uri": "^3.1.0", - "xdg-basedir": "^5.1.0" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/cspell-lib/node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cspell-trie-lib": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/cspell-trie-lib/-/cspell-trie-lib-9.2.1.tgz", - "integrity": "sha512-qOtbL+/tUzGFHH0Uq2wi7sdB9iTy66QNx85P7DKeRdX9ZH53uQd7qC4nEk+/JPclx1EgXX26svxr0jTGISJhLw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@cspell/cspell-pipe": "9.2.1", - "@cspell/cspell-types": "9.2.1", - "gensequence": "^7.0.0" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/cspell/node_modules/chalk": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, "node_modules/debug": { "version": "4.4.1", "license": "MIT", @@ -3792,19 +2886,6 @@ "node": ">=10.13.0" } }, - "node_modules/env-paths": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", - "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/environment": { "version": "1.1.0", "dev": true, @@ -4214,20 +3295,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, - "license": "BSD-2-Clause", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/esquery": { "version": "1.6.0", "dev": true, @@ -4419,16 +3486,6 @@ "dev": true, "license": "Apache-2.0" }, - "node_modules/fast-equals": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.3.2.tgz", - "integrity": "sha512-6rxyATwPCkaFIL3JLqw8qXqMpIZ942pTX/tbQFkRsDGblS8tNGtlUauA/+mt6RUfqn/4MoEr+WDkYoIQbibWuQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/fast-glob": { "version": "3.3.3", "dev": true, @@ -4665,16 +3722,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/gensequence": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/gensequence/-/gensequence-7.0.0.tgz", - "integrity": "sha512-47Frx13aZh01afHJTB3zTtKIlFI6vWY+MYCN9Qpew6i52rfKjnhCF/l1YlC8UmEMvvntZZ6z4PiCcmyuedR2aQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, "node_modules/gensync": { "version": "1.0.0-beta.2", "dev": true, @@ -4790,32 +3837,6 @@ "node": "*" } }, - "node_modules/global-directory": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/global-directory/-/global-directory-4.0.1.tgz", - "integrity": "sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ini": "4.1.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/global-directory/node_modules/ini": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz", - "integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, "node_modules/global-modules": { "version": "1.0.0", "dev": true, @@ -5092,17 +4113,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/import-meta-resolve": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", - "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==", - "dev": true, - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/imurmurhash": { "version": "0.1.4", "dev": true, @@ -7406,19 +6416,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/smol-toml": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.4.2.tgz", - "integrity": "sha512-rInDH6lCNiEyn3+hH8KVGFdbjc099j47+OSgbMrfDYX1CmXLfdKd7qi6IfcWj2wFxvSVkuI46M+wPGYfEOEj6g==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">= 18" - }, - "funding": { - "url": "https://github.com/sponsors/cyyynthia" - } - }, "node_modules/snake-case": { "version": "3.0.4", "dev": true, @@ -8415,20 +7412,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/vscode-languageserver-textdocument": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", - "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==", - "dev": true, - "license": "MIT" - }, - "node_modules/vscode-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", - "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", - "dev": true, - "license": "MIT" - }, "node_modules/walk-up-path": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/walk-up-path/-/walk-up-path-4.0.0.tgz", @@ -8513,28 +7496,13 @@ "version": "1.0.2", "license": "ISC" }, - "node_modules/xdg-basedir": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-5.1.0.tgz", - "integrity": "sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/yallist": { "version": "3.1.1", "dev": true, "license": "ISC" }, "node_modules/yaml": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", - "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", + "version": "2.8.0", "dev": true, "license": "ISC", "bin": { diff --git a/package.json b/package.json index 94ed1b8..28240cd 100644 --- a/package.json +++ b/package.json @@ -10,18 +10,18 @@ "mapbox-mcp-devkit": "dist/esm/index.js" }, "scripts": { - "build": "npm run prepare && tshy && npm run generate-version && node scripts/add-shebang.cjs", - "format": "prettier --check \"./src/**/*.{ts,tsx,js,json,md}\" \"./test/**/*.{ts,tsx,js,json,md}\"", - "format:fix": "prettier --write \"./src/**/*.{ts,tsx,js,json,md}\" \"./test/**/*.{ts,tsx,js,json,md}\"", - "generate-version": "node scripts/build-helpers.cjs generate-version", - "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", "lint": "eslint \"./src/**/*.{ts,tsx}\" \"./test/**/*.{ts,tsx}\"", "lint:fix": "eslint \"./src/**/*.{ts,tsx}\" \"./test/**/*.{ts,tsx}\" --fix", + "fix-lint": "npm run lint:fix && npm run format:fix", + "format": "prettier --check \"./src/**/*.{ts,tsx,js,json,md}\" \"./test/**/*.{ts,tsx,js,json,md}\"", + "format:fix": "prettier --write \"./src/**/*.{ts,tsx,js,json,md}\" \"./test/**/*.{ts,tsx,js,json,md}\"", "prepare": "husky && node .husky/setup-hooks.js", - "spellcheck": "cspell \"*.md\" \"src/**/*.ts\" \"test/**/*.ts\"", + "test": "vitest", + "build": "npm run prepare && npm run sync-manifest && tshy && npm run generate-version && node scripts/build-helpers.cjs copy-json && node scripts/add-shebang.cjs", + "generate-version": "node scripts/build-helpers.cjs generate-version", "sync-manifest": "node scripts/sync-manifest-version.cjs", - "test": "vitest" + "dev": "tsc -p tsconfig.json --watch", + "dev:inspect": "npx @modelcontextprotocol/inspector -e MAPBOX_ACCESS_TOKEN=\"$MAPBOX_ACCESS_PRIVATE_TOKEN\" npx -y tsx src/index.ts" }, "lint-staged": { "*.{js,jsx,ts,tsx}": "eslint --fix", @@ -53,7 +53,6 @@ "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", "@vitest/coverage-istanbul": "^3.2.4", - "cspell": "^9.2.1", "eslint": "^9.0.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-n": "^17.21.3", diff --git a/src/index.ts b/src/index.ts index d38e577..728000a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,3 @@ -// Copyright (c) Mapbox, Inc. -// Licensed under the MIT License. - import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { parseToolConfigFromArgs, filterTools } from './config/toolConfig.js'; diff --git a/src/schemas/style.ts b/src/schemas/style.ts deleted file mode 100644 index 88fcb0e..0000000 --- a/src/schemas/style.ts +++ /dev/null @@ -1,223 +0,0 @@ -// Copyright (c) Mapbox, Inc. -// Licensed under the MIT License. - -import { z } from 'zod'; - -// Basic types -const ColorSchema = z - .string() - .describe('Color as hex, rgb, rgba, hsl, or hsla'); -const CoordinatesSchema = z.tuple([z.number(), z.number()]); - -// Transition schema -const TransitionSchema = z.object({ - duration: z.number().optional(), - delay: z.number().optional() -}); - -// Light schema -const LightSchema = z.object({ - anchor: z.enum(['map', 'viewport']).optional(), - position: z.tuple([z.number(), z.number(), z.number()]).optional(), - color: ColorSchema.optional(), - intensity: z.number().optional() -}); - -// Lights (3D) schema -const LightsSchema = z.array( - z.object({ - id: z.string(), - type: z.enum(['ambient', 'directional']), - properties: z.record(z.any()).optional() - }) -); - -// Terrain schema -const TerrainSchema = z.object({ - source: z.string(), - exaggeration: z.number().optional() -}); - -// Source schemas -const VectorSourceSchema = z.object({ - type: z.literal('vector'), - url: z.string().optional(), - tiles: z.array(z.string()).optional(), - bounds: z.tuple([z.number(), z.number(), z.number(), z.number()]).optional(), - scheme: z.enum(['xyz', 'tms']).optional(), - minzoom: z.number().min(0).max(22).optional(), - maxzoom: z.number().min(0).max(22).optional(), - attribution: z.string().optional(), - promoteId: z.union([z.string(), z.record(z.string())]).optional(), - volatile: z.boolean().optional() -}); - -const RasterSourceSchema = z.object({ - type: z.literal('raster'), - url: z.string().optional(), - tiles: z.array(z.string()).optional(), - bounds: z.tuple([z.number(), z.number(), z.number(), z.number()]).optional(), - minzoom: z.number().min(0).max(22).optional(), - maxzoom: z.number().min(0).max(22).optional(), - tileSize: z.number().optional(), - scheme: z.enum(['xyz', 'tms']).optional(), - attribution: z.string().optional(), - volatile: z.boolean().optional() -}); - -const RasterDemSourceSchema = z.object({ - type: z.literal('raster-dem'), - url: z.string().optional(), - tiles: z.array(z.string()).optional(), - bounds: z.tuple([z.number(), z.number(), z.number(), z.number()]).optional(), - minzoom: z.number().min(0).max(22).optional(), - maxzoom: z.number().min(0).max(22).optional(), - tileSize: z.number().optional(), - attribution: z.string().optional(), - encoding: z.enum(['terrarium', 'mapbox']).optional(), - volatile: z.boolean().optional() -}); - -const GeoJSONSourceSchema = z.object({ - type: z.literal('geojson'), - data: z.union([z.string(), z.any()]), // URL or inline GeoJSON - maxzoom: z.number().min(0).max(24).optional(), - attribution: z.string().optional(), - buffer: z.number().min(0).max(512).optional(), - tolerance: z.number().optional(), - cluster: z.boolean().optional(), - clusterRadius: z.number().optional(), - clusterMaxZoom: z.number().optional(), - clusterProperties: z.record(z.any()).optional(), - lineMetrics: z.boolean().optional(), - generateId: z.boolean().optional(), - promoteId: z.union([z.string(), z.record(z.string())]).optional() -}); - -const ImageSourceSchema = z.object({ - type: z.literal('image'), - url: z.string(), - coordinates: z.tuple([ - CoordinatesSchema, - CoordinatesSchema, - CoordinatesSchema, - CoordinatesSchema - ]) -}); - -const VideoSourceSchema = z.object({ - type: z.literal('video'), - urls: z.array(z.string()), - coordinates: z.tuple([ - CoordinatesSchema, - CoordinatesSchema, - CoordinatesSchema, - CoordinatesSchema - ]) -}); - -const SourceSchema = z.union([ - VectorSourceSchema, - RasterSourceSchema, - RasterDemSourceSchema, - GeoJSONSourceSchema, - ImageSourceSchema, - VideoSourceSchema -]); - -// Layer schema (simplified - full schema would be very extensive) -const LayerSchema = z.object({ - id: z.string().describe('Unique layer name'), - type: z.enum([ - 'fill', - 'line', - 'symbol', - 'circle', - 'heatmap', - 'fill-extrusion', - 'raster', - 'hillshade', - 'background', - 'sky', - 'slot', - 'clip', - 'model', - 'raster-particle', - 'building' - ]), - source: z - .string() - .optional() - .describe('Source name (not required for background/sky/slot)'), - 'source-layer': z - .string() - .optional() - .describe('Layer from vector tile source'), - minzoom: z.number().min(0).max(24).optional(), - maxzoom: z.number().min(0).max(24).optional(), - filter: z.any().optional().describe('Expression for filtering features'), - layout: z.record(z.any()).optional(), - paint: z.record(z.any()).optional(), - metadata: z.record(z.any()).optional(), - slot: z.string().optional().describe('Slot this layer is assigned to') -}); - -// Style import schema -const StyleImportSchema = z.object({ - id: z.string(), - url: z.string(), - config: z.record(z.any()).optional() -}); - -// Base Style properties (shared between input and output) -export const BaseStylePropertiesSchema = z.object({ - // Required Style Spec properties - version: z - .literal(8) - .describe('Style specification version number. Must be 8'), - sources: z.record(SourceSchema).describe('Data source specifications'), - layers: z.array(LayerSchema).describe('Layers in draw order'), - - // Optional Style Spec properties - metadata: z - .record(z.any()) - .optional() - .describe('Arbitrary properties for tracking'), - center: CoordinatesSchema.optional().describe( - 'Default map center [longitude, latitude]' - ), - zoom: z.number().optional().describe('Default zoom level'), - bearing: z.number().optional().describe('Default bearing in degrees'), - pitch: z.number().optional().describe('Default pitch in degrees'), - sprite: z - .string() - .optional() - .describe('Base URL for sprite image and metadata'), - glyphs: z.string().optional().describe('URL template for glyph sets'), - light: LightSchema.optional() - .nullable() - .describe('Global light source (deprecated, use lights)'), - lights: LightsSchema.optional() - .nullable() - .describe('Array of 3D light sources'), - terrain: TerrainSchema.optional() - .nullable() - .describe('Global terrain elevation'), - fog: z.record(z.any()).optional().nullable().describe('Fog properties'), - projection: z - .record(z.any()) - .optional() - .nullable() - .describe('Map projection'), - transition: TransitionSchema.optional() - .nullable() - .describe('Default transition timing'), - imports: z - .array(StyleImportSchema) - .optional() - .nullable() - .describe('Imported styles') -}); - -export type MapboxSource = z.infer; -export type MapboxLayer = z.infer; diff --git a/src/tools/BaseTool.ts b/src/tools/BaseTool.ts index d1d499b..bea2c64 100644 --- a/src/tools/BaseTool.ts +++ b/src/tools/BaseTool.ts @@ -1,62 +1,102 @@ -// Copyright (c) Mapbox, Inc. -// Licensed under the MIT License. - import { McpServer, RegisteredTool } from '@modelcontextprotocol/sdk/server/mcp.js'; import { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol.js'; -import { - CallToolResult, - ToolAnnotations -} from '@modelcontextprotocol/sdk/types.js'; +import { ToolAnnotations } from '@modelcontextprotocol/sdk/types.js'; import { z, ZodTypeAny } from 'zod'; -export abstract class BaseTool< - InputSchema extends ZodTypeAny, - OutputSchema extends ZodTypeAny = ZodTypeAny -> { +const ContentItemSchema = z.union([ + z.object({ + type: z.literal('text'), + text: z.string() + }), + z.object({ + type: z.literal('image'), + data: z.string(), + mimeType: z.string() + }) +]); + +export const OutputSchema = z.object({ + content: z.array(ContentItemSchema), + isError: z.boolean().default(false) +}); + +export type ContentItem = z.infer; +export type ToolOutput = z.infer; + +export abstract class BaseTool { abstract readonly name: string; abstract readonly description: string; abstract readonly annotations: ToolAnnotations; readonly inputSchema: InputSchema; - readonly outputSchema?: OutputSchema; protected server: McpServer | null = null; - constructor(params: { - inputSchema: InputSchema; - outputSchema?: OutputSchema; - }) { + constructor(params: { inputSchema: InputSchema }) { this.inputSchema = params.inputSchema; - this.outputSchema = params.outputSchema; } /** - * Tool logic to be implemented by subclasses. + * Validates and runs the tool logic. */ async run( rawInput: unknown, - // eslint-disable-next-line @typescript-eslint/no-explicit-any extra?: RequestHandlerExtra - ): Promise { + ): Promise> { try { const input = this.inputSchema.parse(rawInput); const accessToken = extra?.authInfo?.token || process.env.MAPBOX_ACCESS_TOKEN; - return this.execute(input, accessToken); + const result = await this.execute(input, accessToken); + + // Check if result is already a content object (image or text) + if ( + result && + typeof result === 'object' && + 'type' in result && + (result.type === 'image' || result.type === 'text') + ) { + return { + content: [result as ContentItem], + isError: false + }; + } + + // Otherwise return as text + return { + content: [{ type: 'text', text: JSON.stringify(result) }], + isError: false + }; } catch (error) { + const errorMessage = + error instanceof Error ? error.message : String(error); + + this.log( + 'error', + `${this.name}: Error during execution: ${errorMessage}` + ); + return { - isError: true, - content: [{ type: 'text', text: (error as Error).message }] + content: [ + { + type: 'text', + text: errorMessage || 'Internal error has occurred.' + } + ], + isError: true }; } } + /** + * Tool logic to be implemented by subclasses. + */ protected abstract execute( - inputSchema: z.infer, + _input: z.infer, accessToken?: string - ): Promise; + ): Promise; /** * Installs the tool to the given MCP server. @@ -64,33 +104,18 @@ export abstract class BaseTool< installTo(server: McpServer): RegisteredTool { this.server = server; - const config: { - title?: string; - description?: string; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - inputSchema?: any; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - outputSchema?: any; - annotations?: ToolAnnotations; - } = { - title: this.annotations.title, - description: this.description, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - inputSchema: (this.inputSchema as unknown as z.ZodObject).shape, - annotations: this.annotations - }; - - // Add outputSchema if provided - if (this.outputSchema) { - // Pass the schema shape directly - don't wrap - // The MCP SDK will validate structuredContent against this schema - config.outputSchema = - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (this.outputSchema as unknown as z.ZodObject).shape; - } - - return server.registerTool(this.name, config, (args, extra) => - this.run(args, extra) + return server.registerTool( + this.name, + { + description: this.description, + inputSchema: ( + this.inputSchema as unknown as z.ZodObject< + Record + > + ).shape, + annotations: this.annotations + }, + (args: any, extra: any) => this.run(args, extra) ); } diff --git a/src/tools/MapboxApiBasedTool.ts b/src/tools/MapboxApiBasedTool.ts index 8bd19e1..2f1a16a 100644 --- a/src/tools/MapboxApiBasedTool.ts +++ b/src/tools/MapboxApiBasedTool.ts @@ -1,31 +1,10 @@ -// Copyright (c) Mapbox, Inc. -// Licensed under the MIT License. - -import type { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol.js'; -import type { ZodTypeAny, z } from 'zod'; -import { BaseTool } from './BaseTool.js'; -import type { - CallToolResult, - ToolAnnotations -} from '@modelcontextprotocol/sdk/types.js'; -import type { HttpRequest } from '../utils/types.js'; - -/** - * Standard error response format from Mapbox API - */ -interface MapboxApiError { - message?: string; - [key: string]: unknown; -} +import { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol.js'; +import { z, ZodTypeAny } from 'zod'; +import { BaseTool, OutputSchema } from './BaseTool.js'; export abstract class MapboxApiBasedTool< - InputSchema extends ZodTypeAny, - OutputSchema extends ZodTypeAny = ZodTypeAny -> extends BaseTool { - abstract readonly name: string; - abstract readonly description: string; - abstract readonly annotations: ToolAnnotations; - + InputSchema extends ZodTypeAny +> extends BaseTool { static get mapboxAccessToken() { return process.env.MAPBOX_ACCESS_TOKEN; } @@ -34,15 +13,51 @@ export abstract class MapboxApiBasedTool< return process.env.MAPBOX_API_ENDPOINT || 'https://api.mapbox.com/'; } - protected httpRequest: HttpRequest; - - constructor(params: { - inputSchema: InputSchema; - outputSchema?: OutputSchema; - httpRequest: HttpRequest; - }) { + constructor(params: { inputSchema: InputSchema }) { super(params); - this.httpRequest = params.httpRequest; + } + + /** + * Extracts the username from the Mapbox access token. + * Mapbox tokens are JWT tokens where the payload contains the username. + * @throws Error if the token is not set, invalid, or doesn't contain username + */ + static getUserNameFromToken(access_token?: string): string { + if (!access_token) { + if (!MapboxApiBasedTool.mapboxAccessToken) { + throw new Error( + 'No access token provided. Please set MAPBOX_ACCESS_TOKEN environment variable or pass it as an argument.' + ); + } + access_token = MapboxApiBasedTool.mapboxAccessToken; + } + + try { + // JWT format: header.payload.signature + const parts = access_token.split('.'); + if (parts.length !== 3) { + throw new Error('MAPBOX_ACCESS_TOKEN is not in valid JWT format'); + } + + // Decode the payload (second part) + const payload = JSON.parse( + Buffer.from(parts[1], 'base64').toString('utf-8') + ); + + // The username is stored in the 'u' field + if (!payload.u) { + throw new Error( + 'MAPBOX_ACCESS_TOKEN does not contain username in payload' + ); + } + + return payload.u; + } catch (error) { + if (error instanceof Error) { + throw error; + } + throw new Error('Failed to parse MAPBOX_ACCESS_TOKEN'); + } } /** @@ -61,13 +76,12 @@ export abstract class MapboxApiBasedTool< } /** - * Validates and runs the tool logic. + * Validates Mapbox token and runs the tool logic. */ async run( rawInput: unknown, - // eslint-disable-next-line @typescript-eslint/no-explicit-any extra?: RequestHandlerExtra - ): Promise { + ): Promise> { try { // First check if token is provided via authentication context // Check both standard token field and accessToken in extra for compatibility @@ -76,28 +90,18 @@ export abstract class MapboxApiBasedTool< const authToken = extra?.authInfo?.token; const accessToken = authToken || MapboxApiBasedTool.mapboxAccessToken; if (!accessToken) { - const errorMessage = - 'No access token available. Please provide via Bearer auth or MAPBOX_ACCESS_TOKEN env var'; - this.log('error', `${this.name}: ${errorMessage}`); - return { - content: [{ type: 'text', text: errorMessage }], - isError: true - }; + throw new Error( + 'No access token available. Please provide via Bearer auth or MAPBOX_ACCESS_TOKEN env var' + ); } // Validate that the token has the correct JWT format if (!this.isValidJwtFormat(accessToken)) { - const errorMessage = 'Access token is not in valid JWT format'; - this.log('error', `${this.name}: ${errorMessage}`); - return { - content: [{ type: 'text', text: errorMessage }], - isError: true - }; + throw new Error('MAPBOX_ACCESS_TOKEN is not in valid JWT format'); } - const input = this.inputSchema.parse(rawInput); - const result = await this.execute(input, accessToken); - return result; + // Call parent run method which handles the rest + return await super.run(rawInput, extra); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); @@ -111,78 +115,11 @@ export abstract class MapboxApiBasedTool< content: [ { type: 'text', - text: errorMessage + text: errorMessage || 'Internal error has occurred.' } ], isError: true }; } } - - /** - * Handles HTTP error responses from Mapbox API. - * Attempts to parse the error response body to extract helpful messages. - * - * @param response - The failed HTTP response - * @param operation - Description of the operation that failed (e.g., "list styles", "create token") - * @returns A CallToolResult with error details - */ - protected async handleApiError( - response: Response, - operation: string - ): Promise { - let errorMessage = `Failed to ${operation}: ${response.status} ${response.statusText}`; - - try { - // Try to parse the response as JSON to get more details - const contentType = response.headers.get('content-type'); - if (contentType && contentType.includes('application/json')) { - const errorData = (await response.json()) as MapboxApiError; - - // Mapbox API typically returns { "message": "..." } for errors - if (errorData.message) { - errorMessage = `Failed to ${operation}: ${errorData.message}`; - - // Check if it's a scope/permission error - if ( - errorData.message.toLowerCase().includes('scope') || - errorData.message.toLowerCase().includes('permission') - ) { - errorMessage += - '\n\nThis operation requires a token with appropriate scopes. Please check your MAPBOX_ACCESS_TOKEN has the necessary permissions.'; - } - } - } else { - // If not JSON, try to get text - const errorText = await response.text(); - if (errorText) { - errorMessage += `\n${errorText}`; - } - } - } catch (parseError) { - // If we can't parse the error body, just use the basic message - this.log('warning', `Failed to parse error response: ${parseError}`); - } - - this.log('error', `${this.name}: ${errorMessage}`); - - return { - content: [ - { - type: 'text', - text: errorMessage - } - ], - isError: true - }; - } - - /** - * Tool logic to be implemented by subclasses. - * Must return a complete OutputSchema with content and optional structured content. - */ - protected abstract execute( - _input: z.infer, - accessToken: string - ): Promise; } diff --git a/src/tools/__snapshots__/tool-naming-convention.test.ts.snap b/src/tools/__snapshots__/tool-naming-convention.test.ts.snap new file mode 100644 index 0000000..204a4e5 --- /dev/null +++ b/src/tools/__snapshots__/tool-naming-convention.test.ts.snap @@ -0,0 +1,86 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Tool Naming Convention should maintain consistent tool list (snapshot test) 1`] = ` +[ + { + "className": "BoundingBoxTool", + "description": "Calculates bounding box of given GeoJSON content, returns as [minX, minY, maxX, maxY]", + "toolName": "bounding_box_tool", + }, + { + "className": "CoordinateConversionTool", + "description": "Converts coordinates between WGS84 (longitude/latitude) and EPSG:3857 (Web Mercator) coordinate systems", + "toolName": "coordinate_conversion_tool", + }, + { + "className": "CountryBoundingBoxTool", + "description": "Gets bounding box for a country by its ISO 3166-1 country code, returns as [minX, minY, maxX, maxY].", + "toolName": "country_bounding_box_tool", + }, + { + "className": "CreateStyleTool", + "description": "Create a new Mapbox style", + "toolName": "create_style_tool", + }, + { + "className": "CreateTokenTool", + "description": "Create a new Mapbox public access token with specified scopes and optional URL restrictions.", + "toolName": "create_token_tool", + }, + { + "className": "DeleteStyleTool", + "description": "Delete a Mapbox style by ID", + "toolName": "delete_style_tool", + }, + { + "className": "GeojsonPreviewTool", + "description": "Generate a geojson.io URL to visualize GeoJSON data. Returns only the URL link.", + "toolName": "geojson_preview_tool", + }, + { + "className": "GetMapboxDocSourceTool", + "description": "Get the latest official Mapbox documentation, APIs, SDKs, and developer resources directly from Mapbox. Always up-to-date, comprehensive coverage of all current Mapbox services including mapping, navigation, search, geocoding, and mobile SDKs. Use this for accurate, official Mapbox information instead of web search.", + "toolName": "get_latest_mapbox_docs_tool", + }, + { + "className": "ListStylesTool", + "description": "List styles for a Mapbox account. Use limit parameter to avoid large responses (recommended: limit=5-10). Use start parameter for pagination.", + "toolName": "list_styles_tool", + }, + { + "className": "ListTokensTool", + "description": "List Mapbox access tokens for the authenticated user with optional filtering and pagination. When using pagination, the "start" parameter must be obtained from the "next_start" field of the previous response (it is not a token ID)", + "toolName": "list_tokens_tool", + }, + { + "className": "PreviewStyleTool", + "description": "Generate preview URL for a Mapbox style using an existing public token", + "toolName": "preview_style_tool", + }, + { + "className": "RetrieveStyleTool", + "description": "Retrieve a specific Mapbox style by ID", + "toolName": "retrieve_style_tool", + }, + { + "className": "StyleComparisonTool", + "description": "Generate a comparison URL for comparing two Mapbox styles side-by-side", + "toolName": "style_comparison_tool", + }, + { + "className": "StyleHelperTool", + "description": "Interactive helper for creating custom Mapbox styles with specific features and colors", + "toolName": "style_helper_tool", + }, + { + "className": "TilequeryTool", + "description": "Query vector and raster data from Mapbox tilesets at geographic coordinates", + "toolName": "tilequery_tool", + }, + { + "className": "UpdateStyleTool", + "description": "Update an existing Mapbox style", + "toolName": "update_style_tool", + }, +] +`; diff --git a/src/tools/bounding-box-tool/BoundariesData-cjs.cts b/src/tools/bounding-box-tool/BoundariesData-cjs.cts index 19cae41..732bed9 100644 --- a/src/tools/bounding-box-tool/BoundariesData-cjs.cts +++ b/src/tools/bounding-box-tool/BoundariesData-cjs.cts @@ -1,6 +1,3 @@ -// Copyright (c) Mapbox, Inc. -// Licensed under the MIT License. - import boundariesData from './boundaries_v4_country_bbox_min.json'; export default boundariesData; diff --git a/src/tools/bounding-box-tool/BoundariesData.ts b/src/tools/bounding-box-tool/BoundariesData.ts index e64d0c9..1a5ba6a 100644 --- a/src/tools/bounding-box-tool/BoundariesData.ts +++ b/src/tools/bounding-box-tool/BoundariesData.ts @@ -1,6 +1,3 @@ -// Copyright (c) Mapbox, Inc. -// Licensed under the MIT License. - // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore import boundariesData from './boundaries_v4_country_bbox_min.json' with { type: 'json' }; diff --git a/src/tools/bounding-box-tool/BoundingBoxTool.output.schema.ts b/src/tools/bounding-box-tool/BoundingBoxTool.output.schema.ts deleted file mode 100644 index c8616b5..0000000 --- a/src/tools/bounding-box-tool/BoundingBoxTool.output.schema.ts +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) Mapbox, Inc. -// Licensed under the MIT License. - -import { z } from 'zod'; - -export const BoundingBoxOutputSchema = z.object({ - bbox: z - .tuple([ - z.number().describe('minX (west longitude)'), - z.number().describe('minY (south latitude)'), - z.number().describe('maxX (east longitude)'), - z.number().describe('maxY (north latitude)') - ]) - .describe('Bounding box as [minX, minY, maxX, maxY]'), - message: z.string().optional().describe('Status or error message') -}); - -export type BoundingBoxOutput = z.infer; diff --git a/src/tools/bounding-box-tool/BoundingBoxTool.input.schema.ts b/src/tools/bounding-box-tool/BoundingBoxTool.schema.ts similarity index 91% rename from src/tools/bounding-box-tool/BoundingBoxTool.input.schema.ts rename to src/tools/bounding-box-tool/BoundingBoxTool.schema.ts index d986f32..03cd826 100644 --- a/src/tools/bounding-box-tool/BoundingBoxTool.input.schema.ts +++ b/src/tools/bounding-box-tool/BoundingBoxTool.schema.ts @@ -1,6 +1,3 @@ -// Copyright (c) Mapbox, Inc. -// Licensed under the MIT License. - import { z } from 'zod'; // Define a loose GeoJSON schema that accepts any valid GeoJSON structure diff --git a/src/tools/bounding-box-tool/BoundingBoxTool.ts b/src/tools/bounding-box-tool/BoundingBoxTool.ts index b5d9900..2cbaa3c 100644 --- a/src/tools/bounding-box-tool/BoundingBoxTool.ts +++ b/src/tools/bounding-box-tool/BoundingBoxTool.ts @@ -1,7 +1,3 @@ -// Copyright (c) Mapbox, Inc. -// Licensed under the MIT License. - -import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; import type { GeoJSON, Feature, @@ -14,13 +10,9 @@ import { BaseTool } from '../BaseTool.js'; import { BoundingBoxSchema, BoundingBoxInput -} from './BoundingBoxTool.input.schema.js'; -import { BoundingBoxOutputSchema } from './BoundingBoxTool.output.schema.js'; +} from './BoundingBoxTool.schema.js'; -export class BoundingBoxTool extends BaseTool< - typeof BoundingBoxSchema, - typeof BoundingBoxOutputSchema -> { +export class BoundingBoxTool extends BaseTool { readonly name = 'bounding_box_tool'; readonly description = 'Calculates bounding box of given GeoJSON content, returns as [minX, minY, maxX, maxY]'; @@ -33,49 +25,26 @@ export class BoundingBoxTool extends BaseTool< }; constructor() { - super({ - inputSchema: BoundingBoxSchema, - outputSchema: BoundingBoxOutputSchema - }); + super({ inputSchema: BoundingBoxSchema }); } - protected async execute(input: BoundingBoxInput): Promise { + protected async execute( + input: BoundingBoxInput + ): Promise<{ type: 'text'; text: string }> { const { geojson } = input; - // Calculate bounding box - let bbox; - try { - // Parse GeoJSON if it's a string - const geojsonObject = - typeof geojson === 'string' - ? (JSON.parse(geojson) as GeoJSON) - : (geojson as GeoJSON); + // Parse GeoJSON if it's a string + const geojsonObject = + typeof geojson === 'string' + ? (JSON.parse(geojson) as GeoJSON) + : (geojson as GeoJSON); - bbox = this.calculateBoundingBox(geojsonObject); - } catch (error) { - return { - content: [ - { - type: 'text', - text: `Error calculating bounding box: ${(error as Error).message}` - } - ], - structuredContent: { - error: (error as Error).message - }, - isError: true - }; - } + // Calculate bounding box + const bbox = this.calculateBoundingBox(geojsonObject); return { - content: [ - { - type: 'text', - text: JSON.stringify({ bbox }, null, 2) - } - ], - structuredContent: { bbox }, - isError: false + type: 'text', + text: JSON.stringify(bbox, null, 2) }; } @@ -162,4 +131,13 @@ export class BoundingBoxTool extends BaseTool< return [minX, minY, maxX, maxY]; } + + private isPosition(coords: unknown): coords is Position { + return ( + Array.isArray(coords) && + coords.length >= 2 && + typeof coords[0] === 'number' && + typeof coords[1] === 'number' + ); + } } diff --git a/src/tools/bounding-box-tool/CountryBoundingBoxTool.output.schema.ts b/src/tools/bounding-box-tool/CountryBoundingBoxTool.output.schema.ts deleted file mode 100644 index e503c00..0000000 --- a/src/tools/bounding-box-tool/CountryBoundingBoxTool.output.schema.ts +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) Mapbox, Inc. -// Licensed under the MIT License. - -import { z } from 'zod'; - -export const CountryBoundingBoxOutputSchema = z.object({ - bbox: z - .tuple([ - z.number().describe('minX (west longitude)'), - z.number().describe('minY (south latitude)'), - z.number().describe('maxX (east longitude)'), - z.number().describe('maxY (north latitude)') - ]) - .describe('Bounding box as [minX, minY, maxX, maxY]'), - message: z.string().optional().describe('Status or error message') -}); - -export type CountryBoundingBoxOutput = z.infer< - typeof CountryBoundingBoxOutputSchema ->; diff --git a/src/tools/bounding-box-tool/CountryBoundingBoxTool.input.schema.ts b/src/tools/bounding-box-tool/CountryBoundingBoxTool.schema.ts similarity index 82% rename from src/tools/bounding-box-tool/CountryBoundingBoxTool.input.schema.ts rename to src/tools/bounding-box-tool/CountryBoundingBoxTool.schema.ts index 2e1b3ef..4d13641 100644 --- a/src/tools/bounding-box-tool/CountryBoundingBoxTool.input.schema.ts +++ b/src/tools/bounding-box-tool/CountryBoundingBoxTool.schema.ts @@ -1,6 +1,3 @@ -// Copyright (c) Mapbox, Inc. -// Licensed under the MIT License. - import { z } from 'zod'; export const CountryBoundingBoxSchema = z.object({ diff --git a/src/tools/bounding-box-tool/CountryBoundingBoxTool.ts b/src/tools/bounding-box-tool/CountryBoundingBoxTool.ts index 57c2dc6..e834169 100644 --- a/src/tools/bounding-box-tool/CountryBoundingBoxTool.ts +++ b/src/tools/bounding-box-tool/CountryBoundingBoxTool.ts @@ -1,18 +1,12 @@ -// Copyright (c) Mapbox, Inc. -// Licensed under the MIT License. - -import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; import { BaseTool } from '../BaseTool.js'; import { CountryBoundingBoxSchema, CountryBoundingBoxInput -} from './CountryBoundingBoxTool.input.schema.js'; -import { CountryBoundingBoxOutputSchema } from './CountryBoundingBoxTool.output.schema.js'; +} from './CountryBoundingBoxTool.schema.js'; import boundariesData from './BoundariesData.js'; export class CountryBoundingBoxTool extends BaseTool< - typeof CountryBoundingBoxSchema, - typeof CountryBoundingBoxOutputSchema + typeof CountryBoundingBoxSchema > { readonly name = 'country_bounding_box_tool'; readonly description = @@ -32,40 +26,25 @@ export class CountryBoundingBoxTool extends BaseTool< >; constructor() { - super({ - inputSchema: CountryBoundingBoxSchema, - outputSchema: CountryBoundingBoxOutputSchema - }); + super({ inputSchema: CountryBoundingBoxSchema }); } protected async execute( input: CountryBoundingBoxInput - ): Promise { + ): Promise<{ type: 'text'; text: string }> { const { iso_3166_1 } = input; const upperCaseCode = iso_3166_1.toUpperCase(); const bbox = this.boundariesData[upperCaseCode]; if (!bbox) { - return { - content: [ - { - type: 'text', - text: `Country code "${iso_3166_1}" not found. Please use a valid ISO 3166-1 country code (e.g., "CN", "US", "AE").` - } - ], - isError: true - }; + throw new Error( + `Country code "${iso_3166_1}" not found. Please use a valid ISO 3166-1 country code (e.g., "CN", "US", "AE").` + ); } return { - content: [ - { - type: 'text', - text: JSON.stringify({ bbox }, null, 2) - } - ], - structuredContent: { bbox }, - isError: false + type: 'text', + text: JSON.stringify(bbox, null, 2) }; } diff --git a/src/tools/coordinate-conversion-tool/CoordinateConversionTool.output.schema.ts b/src/tools/coordinate-conversion-tool/CoordinateConversionTool.output.schema.ts deleted file mode 100644 index aa1d99d..0000000 --- a/src/tools/coordinate-conversion-tool/CoordinateConversionTool.output.schema.ts +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) Mapbox, Inc. -// Licensed under the MIT License. - -import { z } from 'zod'; - -export const CoordinateConversionOutputSchema = z.object({ - input: z.array(z.number()).length(2).describe('Input coordinates'), - output: z.array(z.number()).length(2).describe('Converted coordinates'), - from: z.enum(['wgs84', 'epsg3857']).describe('Source coordinate system'), - to: z.enum(['wgs84', 'epsg3857']).describe('Target coordinate system'), - message: z.string().optional().describe('Conversion status message') -}); - -export type CoordinateConversionOutput = z.infer< - typeof CoordinateConversionOutputSchema ->; diff --git a/src/tools/coordinate-conversion-tool/CoordinateConversionTool.input.schema.ts b/src/tools/coordinate-conversion-tool/CoordinateConversionTool.schema.ts similarity index 90% rename from src/tools/coordinate-conversion-tool/CoordinateConversionTool.input.schema.ts rename to src/tools/coordinate-conversion-tool/CoordinateConversionTool.schema.ts index 1e3255f..7ab9b66 100644 --- a/src/tools/coordinate-conversion-tool/CoordinateConversionTool.input.schema.ts +++ b/src/tools/coordinate-conversion-tool/CoordinateConversionTool.schema.ts @@ -1,6 +1,3 @@ -// Copyright (c) Mapbox, Inc. -// Licensed under the MIT License. - import { z } from 'zod'; export const CoordinateConversionSchema = z.object({ diff --git a/src/tools/coordinate-conversion-tool/CoordinateConversionTool.ts b/src/tools/coordinate-conversion-tool/CoordinateConversionTool.ts index c14f5a0..aab9c64 100644 --- a/src/tools/coordinate-conversion-tool/CoordinateConversionTool.ts +++ b/src/tools/coordinate-conversion-tool/CoordinateConversionTool.ts @@ -1,20 +1,11 @@ -// Copyright (c) Mapbox, Inc. -// Licensed under the MIT License. - -import { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; import { BaseTool } from '../BaseTool.js'; -import { - CoordinateConversionOutput, - CoordinateConversionOutputSchema -} from './CoordinateConversionTool.output.schema.js'; import { CoordinateConversionSchema, CoordinateConversionInput -} from './CoordinateConversionTool.input.schema.js'; +} from './CoordinateConversionTool.schema.js'; export class CoordinateConversionTool extends BaseTool< - typeof CoordinateConversionSchema, - typeof CoordinateConversionOutputSchema + typeof CoordinateConversionSchema > { readonly name = 'coordinate_conversion_tool'; readonly description = @@ -28,90 +19,53 @@ export class CoordinateConversionTool extends BaseTool< }; constructor() { - super({ - inputSchema: CoordinateConversionSchema, - outputSchema: CoordinateConversionOutputSchema - }); + super({ inputSchema: CoordinateConversionSchema }); } protected async execute( input: CoordinateConversionInput - ): Promise { + ): Promise<{ type: 'text'; text: string }> { const { coordinates, from, to } = input; if (from === to) { - const outputResult: CoordinateConversionOutput = { - input: coordinates, - output: coordinates, - from, - to, - message: 'No conversion needed - source and target are the same' - }; - return { - content: [ + type: 'text', + text: JSON.stringify( { - type: 'text', - text: JSON.stringify(outputResult, null, 2) - } - ], - isError: false, - structuredContent: outputResult + input: coordinates, + output: coordinates, + from, + to, + message: 'No conversion needed - source and target are the same' + }, + null, + 2 + ) }; } let result: [number, number]; - const method = - from === 'wgs84' && to === 'epsg3857' - ? this.wgs84ToEpsg3857.bind(this) - : from === 'epsg3857' && to === 'wgs84' - ? this.epsg3857ToWgs84.bind(this) - : undefined; - - if (!method) { - return { - content: [ - { - type: 'text', - text: `Unsupported conversion: ${from} to ${to}` - } - ], - isError: true - }; + if (from === 'wgs84' && to === 'epsg3857') { + result = this.wgs84ToEpsg3857(coordinates[0], coordinates[1]); + } else if (from === 'epsg3857' && to === 'wgs84') { + result = this.epsg3857ToWgs84(coordinates[0], coordinates[1]); + } else { + throw new Error(`Unsupported conversion: ${from} to ${to}`); } - try { - result = method(coordinates[0], coordinates[1]); - } catch (error) { - return { - content: [ - { - type: 'text', - text: `Error during conversion: ${(error as Error).message}` - } - ], - isError: true - }; - } - - const outputResult: CoordinateConversionOutput = { - input: coordinates, - output: result, - from, - to, - message: 'Conversion successful' - }; - return { - content: [ + type: 'text', + text: JSON.stringify( { - type: 'text', - text: JSON.stringify(outputResult, null, 2) - } - ], - isError: false, - structuredContent: outputResult + input: coordinates, + output: result, + from, + to + }, + null, + 2 + ) }; } diff --git a/src/tools/create-style-tool/CreateStyleTool.input.schema.ts b/src/tools/create-style-tool/CreateStyleTool.input.schema.ts deleted file mode 100644 index a3d5bce..0000000 --- a/src/tools/create-style-tool/CreateStyleTool.input.schema.ts +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) Mapbox, Inc. -// Licensed under the MIT License. - -import { z } from 'zod'; -import { BaseStylePropertiesSchema } from '../../schemas/style.js'; - -// INPUT Schema - For creating/updating styles (PATCH/POST request body) -export const MapboxStyleInputSchema = BaseStylePropertiesSchema.extend({ - name: z - .string() - .describe('Human-readable name for the style (REQUIRED for updates)') - // These fields should NOT be included in input - they're read-only - // If present, they'll be ignored or cause API errors -}).passthrough(); - -// Type exports -export type MapboxStyleInput = z.infer; diff --git a/src/tools/create-style-tool/CreateStyleTool.output.schema.ts b/src/tools/create-style-tool/CreateStyleTool.output.schema.ts deleted file mode 100644 index 91a2602..0000000 --- a/src/tools/create-style-tool/CreateStyleTool.output.schema.ts +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) Mapbox, Inc. -// Licensed under the MIT License. - -import { z } from 'zod'; -import { BaseStylePropertiesSchema } from '../../schemas/style.js'; - -// OUTPUT Schema - For API responses (GET/PATCH response) -// TODO: Refactor into shared schema -export const MapboxStyleOutputSchema = BaseStylePropertiesSchema.extend({ - name: z.string().describe('Human-readable name for the style'), - - // API-specific properties (only present in responses) - id: z.string().describe('Unique style identifier'), - owner: z.string().describe('Username of the style owner'), - created: z - .string() - .datetime() - .describe('ISO 8601 timestamp when style was created'), - modified: z - .string() - .datetime() - .describe('ISO 8601 timestamp when style was last modified'), - visibility: z - .enum(['public', 'private']) - .describe('Style visibility setting'), - draft: z.boolean().optional().describe('Whether this is a draft version') -}).passthrough(); - -// Type exports -export type MapboxStyleOutput = z.infer; diff --git a/src/tools/create-style-tool/CreateStyleTool.schema.ts b/src/tools/create-style-tool/CreateStyleTool.schema.ts new file mode 100644 index 0000000..2e5c606 --- /dev/null +++ b/src/tools/create-style-tool/CreateStyleTool.schema.ts @@ -0,0 +1,8 @@ +import { z } from 'zod'; + +export const CreateStyleSchema = z.object({ + name: z.string().describe('Name for the new style'), + style: z.record(z.any()).describe('Mapbox style specification object') +}); + +export type CreateStyleInput = z.infer; diff --git a/src/tools/create-style-tool/CreateStyleTool.ts b/src/tools/create-style-tool/CreateStyleTool.ts index edf0f90..f1dbdb7 100644 --- a/src/tools/create-style-tool/CreateStyleTool.ts +++ b/src/tools/create-style-tool/CreateStyleTool.ts @@ -1,23 +1,13 @@ -// Copyright (c) Mapbox, Inc. -// Licensed under the MIT License. - -import { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; -import type { HttpRequest } from '../../utils/types.js'; -import { getUserNameFromToken } from '../../utils/jwtUtils.js'; +import { fetchClient } from '../../utils/fetchRequest.js'; import { filterExpandedMapboxStyles } from '../../utils/styleUtils.js'; import { MapboxApiBasedTool } from '../MapboxApiBasedTool.js'; import { - MapboxStyleInputSchema, - MapboxStyleInput -} from './CreateStyleTool.input.schema.js'; -import { - MapboxStyleOutput, - MapboxStyleOutputSchema -} from './CreateStyleTool.output.schema.js'; + CreateStyleSchema, + CreateStyleInput +} from './CreateStyleTool.schema.js'; export class CreateStyleTool extends MapboxApiBasedTool< - typeof MapboxStyleInputSchema, - typeof MapboxStyleOutputSchema + typeof CreateStyleSchema > { name = 'create_style_tool'; description = 'Create a new Mapbox style'; @@ -29,58 +19,38 @@ export class CreateStyleTool extends MapboxApiBasedTool< title: 'Create Mapbox Style Tool' }; - constructor(params: { httpRequest: HttpRequest }) { - super({ - inputSchema: MapboxStyleInputSchema, - outputSchema: MapboxStyleOutputSchema, - httpRequest: params.httpRequest - }); + constructor(private fetch: typeof globalThis.fetch = fetchClient) { + super({ inputSchema: CreateStyleSchema }); } protected async execute( - input: MapboxStyleInput, + input: CreateStyleInput, accessToken?: string - ): Promise { - const username = getUserNameFromToken(accessToken); + ): Promise { + const username = MapboxApiBasedTool.getUserNameFromToken(accessToken); const url = `${MapboxApiBasedTool.mapboxApiEndpoint}styles/v1/${username}?access_token=${accessToken}`; - const response = await this.httpRequest(url, { + const payload = { + name: input.name, + ...input.style + }; + + const response = await this.fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(input) + body: JSON.stringify(payload) }); if (!response.ok) { - return this.handleApiError(response, 'create style'); - } - - const rawData = await response.json(); - // Validate response against schema with graceful fallback - let data: MapboxStyleOutput; - try { - data = MapboxStyleOutputSchema.parse(rawData); - } catch (validationError) { - this.log( - 'warning', - `Schema validation failed for search response: ${validationError instanceof Error ? validationError.message : 'Unknown validation error'}` + throw new Error( + `Failed to create style: ${response.status} ${response.statusText}` ); - // Graceful fallback to raw data - data = rawData as MapboxStyleOutput; } - this.log('info', `CreateStyleTool: Successfully created style ${data.id}`); - - return { - content: [ - { - type: 'text', - text: JSON.stringify(filterExpandedMapboxStyles(data), null, 2) - } - ], - structuredContent: filterExpandedMapboxStyles(data), - isError: false - }; + const data = await response.json(); + // Return full style but filter out expanded Mapbox styles + return filterExpandedMapboxStyles(data); } } diff --git a/src/tools/create-token-tool/CreateTokenTool.output.schema.ts b/src/tools/create-token-tool/CreateTokenTool.output.schema.ts deleted file mode 100644 index 370d0f4..0000000 --- a/src/tools/create-token-tool/CreateTokenTool.output.schema.ts +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) Mapbox, Inc. -// Licensed under the MIT License. - -import { z } from 'zod'; - -export const CreateTokenOutputSchema = z.object({ - id: z.string().describe("The token's unique identifier"), - usage: z - .enum(['pk', 'sk', 'tk']) - .describe('Token usage type: pk (public), sk (secret), or tk (temporary)'), - client: z.string().describe('The client for the token'), - default: z.boolean().describe('Whether this is the default token'), - scopes: z.array(z.string()).describe('Array of scopes granted to the token'), - note: z - .string() - .nullable() - .describe('Human-readable description of the token'), - created: z.string().describe('ISO 8601 creation timestamp'), - modified: z.string().describe('ISO 8601 last modified timestamp'), - allowedUrls: z - .array(z.string()) - .optional() - .describe('URLs that the token is restricted to'), - token: z.string().describe('The actual access token string'), - expires: z - .string() - .optional() - .describe('Expiration time in ISO 8601 format (temporary tokens only)') -}); - -export type CreateTokenOutput = z.infer; diff --git a/src/tools/create-token-tool/CreateTokenTool.input.schema.ts b/src/tools/create-token-tool/CreateTokenTool.schema.ts similarity index 93% rename from src/tools/create-token-tool/CreateTokenTool.input.schema.ts rename to src/tools/create-token-tool/CreateTokenTool.schema.ts index 5ab815a..5e58d92 100644 --- a/src/tools/create-token-tool/CreateTokenTool.input.schema.ts +++ b/src/tools/create-token-tool/CreateTokenTool.schema.ts @@ -1,6 +1,3 @@ -// Copyright (c) Mapbox, Inc. -// Licensed under the MIT License. - import { z } from 'zod'; // Valid scopes for public tokens diff --git a/src/tools/create-token-tool/CreateTokenTool.ts b/src/tools/create-token-tool/CreateTokenTool.ts index a32b47b..d242550 100644 --- a/src/tools/create-token-tool/CreateTokenTool.ts +++ b/src/tools/create-token-tool/CreateTokenTool.ts @@ -1,19 +1,12 @@ -// Copyright (c) Mapbox, Inc. -// Licensed under the MIT License. - -import { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; -import type { HttpRequest } from '../../utils/types.js'; +import { fetchClient } from '../../utils/fetchRequest.js'; import { MapboxApiBasedTool } from '../MapboxApiBasedTool.js'; import { CreateTokenSchema, CreateTokenInput -} from './CreateTokenTool.input.schema.js'; -import { CreateTokenOutputSchema } from './CreateTokenTool.output.schema.js'; -import { getUserNameFromToken } from '../../utils/jwtUtils.js'; +} from './CreateTokenTool.schema.js'; export class CreateTokenTool extends MapboxApiBasedTool< - typeof CreateTokenSchema, - typeof CreateTokenOutputSchema + typeof CreateTokenSchema > { readonly name = 'create_token_tool'; readonly description = @@ -26,19 +19,15 @@ export class CreateTokenTool extends MapboxApiBasedTool< title: 'Create Mapbox Token Tool' }; - constructor(params: { httpRequest: HttpRequest }) { - super({ - inputSchema: CreateTokenSchema, - outputSchema: CreateTokenOutputSchema, - httpRequest: params.httpRequest - }); + constructor(private fetch: typeof globalThis.fetch = fetchClient) { + super({ inputSchema: CreateTokenSchema }); } protected async execute( input: CreateTokenInput, accessToken?: string - ): Promise { - const username = getUserNameFromToken(accessToken); + ): Promise<{ type: 'text'; text: string }> { + const username = MapboxApiBasedTool.getUserNameFromToken(accessToken); this.log( 'info', @@ -59,15 +48,7 @@ export class CreateTokenTool extends MapboxApiBasedTool< if (input.allowedUrls) { if (input.allowedUrls.length > 100) { - return { - content: [ - { - type: 'text', - text: 'Maximum 100 allowed URLs per token' - } - ], - isError: true - }; + throw new Error('Maximum 100 allowed URLs per token'); } body.allowedUrls = input.allowedUrls; } @@ -76,46 +57,38 @@ export class CreateTokenTool extends MapboxApiBasedTool< body.expires = input.expires; } - const response = await this.httpRequest(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(body) - }); + try { + const response = await this.fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(body) + }); - if (!response.ok) { - return this.handleApiError(response, 'create token'); - } + if (!response.ok) { + const errorBody = await response.text(); + this.log( + 'error', + `CreateTokenTool: API Error - Status: ${response.status}, Body: ${errorBody}` + ); + throw new Error( + `Failed to create token: ${response.status} ${response.statusText}` + ); + } + + const data = await response.json(); + this.log('info', `CreateTokenTool: Successfully created token`); - const data = await response.json(); - const parseResult = CreateTokenOutputSchema.safeParse(data); - if (!parseResult.success) { - this.log( - 'error', - `CreateTokenTool: Output schema validation failed\n${parseResult.error}` - ); return { - content: [ - { - type: 'text', - text: `CreateTokenTool: Response does not conform to output schema:\n${parseResult.error}` - } - ], - isError: true + type: 'text', + text: JSON.stringify(data, null, 2) }; + } catch (error) { + if (error instanceof Error) { + throw error; + } + throw new Error(`Failed to create token: ${String(error)}`); } - this.log('info', `CreateTokenTool: Successfully created token`); - - return { - content: [ - { - type: 'text', - text: JSON.stringify(parseResult.data, null, 2) - } - ], - structuredContent: parseResult.data, - isError: false - }; } } diff --git a/src/tools/delete-style-tool/DeleteStyleTool.input.schema.ts b/src/tools/delete-style-tool/DeleteStyleTool.schema.ts similarity index 100% rename from src/tools/delete-style-tool/DeleteStyleTool.input.schema.ts rename to src/tools/delete-style-tool/DeleteStyleTool.schema.ts diff --git a/src/tools/delete-style-tool/DeleteStyleTool.ts b/src/tools/delete-style-tool/DeleteStyleTool.ts index 1595373..1be3a4c 100644 --- a/src/tools/delete-style-tool/DeleteStyleTool.ts +++ b/src/tools/delete-style-tool/DeleteStyleTool.ts @@ -1,14 +1,9 @@ -// Copyright (c) Mapbox, Inc. -// Licensed under the MIT License. - -import { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; -import type { HttpRequest } from '../../utils/types.js'; -import { getUserNameFromToken } from '../../utils/jwtUtils.js'; +import { fetchClient } from '../../utils/fetchRequest.js'; import { MapboxApiBasedTool } from '../MapboxApiBasedTool.js'; import { DeleteStyleSchema, DeleteStyleInput -} from './DeleteStyleTool.input.schema.js'; +} from './DeleteStyleTool.schema.js'; export class DeleteStyleTool extends MapboxApiBasedTool< typeof DeleteStyleSchema @@ -23,33 +18,33 @@ export class DeleteStyleTool extends MapboxApiBasedTool< title: 'Delete Mapbox Style Tool' }; - constructor(params: { httpRequest: HttpRequest }) { - super({ inputSchema: DeleteStyleSchema, httpRequest: params.httpRequest }); + constructor(private fetch: typeof globalThis.fetch = fetchClient) { + super({ inputSchema: DeleteStyleSchema }); } protected async execute( input: DeleteStyleInput, accessToken?: string - ): Promise { - const username = getUserNameFromToken(accessToken); + ): Promise { + const username = MapboxApiBasedTool.getUserNameFromToken(accessToken); const url = `${MapboxApiBasedTool.mapboxApiEndpoint}styles/v1/${username}/${input.styleId}?access_token=${accessToken}`; - const response = await this.httpRequest(url, { + const response = await this.fetch(url, { method: 'DELETE' }); - if (response.status !== 204) { - return this.handleApiError(response, 'delete style'); + if (!response.ok) { + throw new Error( + `Failed to delete style: ${response.status} ${response.statusText}` + ); + } + + // Delete typically returns 204 No Content + if (response.status === 204) { + return { success: true, message: 'Style deleted successfully' }; } - return { - content: [ - { - type: 'text', - text: 'Style deleted successfully' - } - ], - isError: false - }; + const data = await response.json(); + return data; } } diff --git a/src/tools/geojson-preview-tool/GeojsonPreviewTool.input.schema.ts b/src/tools/geojson-preview-tool/GeojsonPreviewTool.schema.ts similarity index 87% rename from src/tools/geojson-preview-tool/GeojsonPreviewTool.input.schema.ts rename to src/tools/geojson-preview-tool/GeojsonPreviewTool.schema.ts index fb8d27e..45a19ee 100644 --- a/src/tools/geojson-preview-tool/GeojsonPreviewTool.input.schema.ts +++ b/src/tools/geojson-preview-tool/GeojsonPreviewTool.schema.ts @@ -1,6 +1,3 @@ -// Copyright (c) Mapbox, Inc. -// Licensed under the MIT License. - import { z } from 'zod'; // Simplified GeoJSON schema for maximum MCP client compatibility diff --git a/src/tools/geojson-preview-tool/GeojsonPreviewTool.ts b/src/tools/geojson-preview-tool/GeojsonPreviewTool.ts index 6d1e640..cc573cd 100644 --- a/src/tools/geojson-preview-tool/GeojsonPreviewTool.ts +++ b/src/tools/geojson-preview-tool/GeojsonPreviewTool.ts @@ -1,13 +1,9 @@ -// Copyright (c) Mapbox, Inc. -// Licensed under the MIT License. - -import { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; import { GeoJSON } from 'geojson'; import { BaseTool } from '../BaseTool.js'; import { GeojsonPreviewSchema, GeojsonPreviewInput -} from './GeojsonPreviewTool.input.schema.js'; +} from './GeojsonPreviewTool.schema.js'; export class GeojsonPreviewTool extends BaseTool { name = 'geojson_preview_tool'; @@ -49,22 +45,16 @@ export class GeojsonPreviewTool extends BaseTool { ); } - protected async execute(input: GeojsonPreviewInput): Promise { + protected async execute( + input: GeojsonPreviewInput + ): Promise<{ type: 'text'; text: string }> { try { // Parse and validate JSON format const geojsonData = JSON.parse(input.geojson); // Validate GeoJSON structure if (!this.isValidGeoJSON(geojsonData)) { - return { - isError: true, - content: [ - { - type: 'text', - text: 'GeoJSON processing failed: Invalid GeoJSON structure' - } - ] - }; + throw new Error('Invalid GeoJSON structure'); } // Generate geojson.io URL @@ -73,26 +63,13 @@ export class GeojsonPreviewTool extends BaseTool { const geojsonIOUrl = `https://geojson.io/#data=data:application/json,${encodedGeoJSON}`; return { - isError: false, - content: [ - { - type: 'text', - text: geojsonIOUrl - } - ] + type: 'text', + text: geojsonIOUrl }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; - return { - isError: true, - content: [ - { - type: 'text', - text: `GeoJSON processing failed: ${errorMessage}` - } - ] - }; + throw new Error(`GeoJSON processing failed: ${errorMessage}`); } } } diff --git a/src/tools/get-mapbox-doc-source-tool/GetMapboxDocSourceTool.input.schema.ts b/src/tools/get-mapbox-doc-source-tool/GetMapboxDocSourceTool.schema.ts similarity index 70% rename from src/tools/get-mapbox-doc-source-tool/GetMapboxDocSourceTool.input.schema.ts rename to src/tools/get-mapbox-doc-source-tool/GetMapboxDocSourceTool.schema.ts index 7701ebd..ccc29ef 100644 --- a/src/tools/get-mapbox-doc-source-tool/GetMapboxDocSourceTool.input.schema.ts +++ b/src/tools/get-mapbox-doc-source-tool/GetMapboxDocSourceTool.schema.ts @@ -1,6 +1,3 @@ -// Copyright (c) Mapbox, Inc. -// Licensed under the MIT License. - import { z } from 'zod'; export const GetMapboxDocSourceSchema = z.object({}); diff --git a/src/tools/get-mapbox-doc-source-tool/GetMapboxDocSourceTool.ts b/src/tools/get-mapbox-doc-source-tool/GetMapboxDocSourceTool.ts index ad7cb00..e1df7f2 100644 --- a/src/tools/get-mapbox-doc-source-tool/GetMapboxDocSourceTool.ts +++ b/src/tools/get-mapbox-doc-source-tool/GetMapboxDocSourceTool.ts @@ -1,13 +1,9 @@ -// Copyright (c) Mapbox, Inc. -// Licensed under the MIT License. - -import { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; -import type { HttpRequest } from '../../utils/types.js'; +import { fetchClient } from '../../utils/fetchRequest.js'; import { BaseTool } from '../BaseTool.js'; import { GetMapboxDocSourceSchema, GetMapboxDocSourceInput -} from './GetMapboxDocSourceTool.input.schema.js'; +} from './GetMapboxDocSourceTool.schema.js'; export class GetMapboxDocSourceTool extends BaseTool< typeof GetMapboxDocSourceSchema @@ -23,59 +19,31 @@ export class GetMapboxDocSourceTool extends BaseTool< title: 'Get Mapbox Documentation Tool' }; - private httpRequest: HttpRequest; - - constructor(params: { httpRequest: HttpRequest }) { - super({ - inputSchema: GetMapboxDocSourceSchema - }); - this.httpRequest = params.httpRequest; + constructor(private fetch: typeof globalThis.fetch = fetchClient) { + super({ inputSchema: GetMapboxDocSourceSchema }); } protected async execute( // eslint-disable-next-line @typescript-eslint/no-unused-vars _input: GetMapboxDocSourceInput - ): Promise { + ): Promise<{ type: 'text'; text: string }> { try { - const response = await this.httpRequest( - 'https://docs.mapbox.com/llms.txt' - ); + const response = await this.fetch('https://docs.mapbox.com/llms.txt'); if (!response.ok) { - return { - content: [ - { - type: 'text', - text: `Failed to fetch Mapbox documentation: ${response.statusText}` - } - ], - isError: true - }; + throw new Error(`HTTP error! status: ${response.status}`); } const content = await response.text(); return { - content: [ - { - type: 'text', - text: content - } - ], - isError: false + type: 'text', + text: content }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; - return { - content: [ - { - type: 'text', - text: `Failed to fetch Mapbox documentation: ${errorMessage}` - } - ], - isError: true - }; + throw new Error(`Failed to fetch Mapbox documentation: ${errorMessage}`); } } } diff --git a/src/tools/list-styles-tool/ListStylesTool.output.schema.ts b/src/tools/list-styles-tool/ListStylesTool.output.schema.ts deleted file mode 100644 index 3c73b6d..0000000 --- a/src/tools/list-styles-tool/ListStylesTool.output.schema.ts +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright (c) Mapbox, Inc. -// Licensed under the MIT License. - -import { z } from 'zod'; - -/** - * Schema for style metadata returned by the list styles endpoint. - * Note: This is different from a full style specification - it contains - * metadata about the style but may not include all style properties like layers. - */ -const StyleMetadataSchema = z.object({ - // Core metadata fields always present - id: z.string().describe('Unique style ID'), - name: z.string().describe('Style name'), - owner: z.string().describe('Username of the style owner'), - created: z.string().describe('ISO 8601 timestamp of creation'), - modified: z.string().describe('ISO 8601 timestamp of last modification'), - visibility: z - .enum(['public', 'private']) - .describe('Style visibility setting'), - - // Optional Style Spec fields that may be included - version: z.literal(8).optional().describe('Style specification version'), - center: z - .tuple([z.number(), z.number()]) - .optional() - .describe('Default center [longitude, latitude]'), - zoom: z.number().optional().describe('Default zoom level'), - bearing: z.number().optional().describe('Default bearing in degrees'), - pitch: z.number().optional().describe('Default pitch in degrees'), - - // Sources and layers may or may not be included in list responses - sources: z.record(z.any()).optional().describe('Style data sources'), - layers: z.array(z.any()).optional().describe('Style layers'), - - // Additional metadata fields - protected: z.boolean().optional().describe('Whether style is protected'), - draft: z.boolean().optional().describe('Whether style is a draft') -}); - -// API returns an array of styles -const StylesArraySchema = z.array(StyleMetadataSchema); - -// But structuredContent wraps it in an object -export const ListStylesOutputSchema = z.object({ - styles: StylesArraySchema -}); - -export type ListStylesOutput = z.infer; -export { StylesArraySchema }; diff --git a/src/tools/list-styles-tool/ListStylesTool.input.schema.ts b/src/tools/list-styles-tool/ListStylesTool.schema.ts similarity index 87% rename from src/tools/list-styles-tool/ListStylesTool.input.schema.ts rename to src/tools/list-styles-tool/ListStylesTool.schema.ts index adf6e12..e2715ba 100644 --- a/src/tools/list-styles-tool/ListStylesTool.input.schema.ts +++ b/src/tools/list-styles-tool/ListStylesTool.schema.ts @@ -1,6 +1,3 @@ -// Copyright (c) Mapbox, Inc. -// Licensed under the MIT License. - import { z } from 'zod'; export const ListStylesSchema = z.object({ diff --git a/src/tools/list-styles-tool/ListStylesTool.ts b/src/tools/list-styles-tool/ListStylesTool.ts index a32e6c2..d0d1b52 100644 --- a/src/tools/list-styles-tool/ListStylesTool.ts +++ b/src/tools/list-styles-tool/ListStylesTool.ts @@ -1,22 +1,9 @@ -// Copyright (c) Mapbox, Inc. -// Licensed under the MIT License. - -import { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; -import type { HttpRequest } from '../../utils/types.js'; +import { fetchClient } from '../../utils/fetchRequest.js'; import { MapboxApiBasedTool } from '../MapboxApiBasedTool.js'; -import { - ListStylesSchema, - ListStylesInput -} from './ListStylesTool.input.schema.js'; -import { - ListStylesOutputSchema, - StylesArraySchema -} from './ListStylesTool.output.schema.js'; -import { getUserNameFromToken } from '../../utils/jwtUtils.js'; +import { ListStylesSchema, ListStylesInput } from './ListStylesTool.schema.js'; export class ListStylesTool extends MapboxApiBasedTool< - typeof ListStylesSchema, - typeof ListStylesOutputSchema + typeof ListStylesSchema > { name = 'list_styles_tool'; description = @@ -29,19 +16,15 @@ export class ListStylesTool extends MapboxApiBasedTool< title: 'List Mapbox Styles Tool' }; - constructor(params: { httpRequest: HttpRequest }) { - super({ - inputSchema: ListStylesSchema, - outputSchema: ListStylesOutputSchema, - httpRequest: params.httpRequest - }); + constructor(private fetch: typeof globalThis.fetch = fetchClient) { + super({ inputSchema: ListStylesSchema }); } protected async execute( input: ListStylesInput, accessToken?: string - ): Promise { - const username = getUserNameFromToken(accessToken); + ): Promise { + const username = MapboxApiBasedTool.getUserNameFromToken(accessToken); // Build query parameters const params = new URLSearchParams(); @@ -60,42 +43,15 @@ export class ListStylesTool extends MapboxApiBasedTool< const url = `${MapboxApiBasedTool.mapboxApiEndpoint}styles/v1/${username}?${params.toString()}`; - const response = await this.httpRequest(url); + const response = await this.fetch(url); if (!response.ok) { - return this.handleApiError(response, 'list styles'); - } - - const data = await response.json(); - // Validate the API response (which is an array) - const parseResult = StylesArraySchema.safeParse(data); - if (!parseResult.success) { - this.log( - 'error', - `ListStylesTool: Output schema validation failed\n${parseResult.error}` + throw new Error( + `Failed to list styles: ${response.status} ${response.statusText}` ); - return { - content: [ - { - type: 'text' as const, - text: `ListStylesTool: Response does not conform to output schema:\n${parseResult.error}` - } - ], - isError: true - }; } - this.log('info', `ListStylesTool: Successfully listed styles`); - const wrappedData = { styles: parseResult.data }; - return { - content: [ - { - type: 'text' as const, - text: JSON.stringify(wrappedData, null, 2) - } - ], - structuredContent: wrappedData, - isError: false - }; + const data = await response.json(); + return data; } } diff --git a/src/tools/list-tokens-tool/ListTokensTool.output.schema.ts b/src/tools/list-tokens-tool/ListTokensTool.output.schema.ts deleted file mode 100644 index 15c5568..0000000 --- a/src/tools/list-tokens-tool/ListTokensTool.output.schema.ts +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (c) Mapbox, Inc. -// Licensed under the MIT License. - -import { z } from 'zod'; - -export const TokenObjectSchema = z.object({ - id: z.string().describe("The token's unique identifier"), - usage: z - .enum(['pk', 'sk', 'tk']) - .describe('Token usage type: pk (public), sk (secret), or tk (temporary)'), - client: z.string().describe('The client for the token'), - default: z.boolean().describe('Whether this is the default token'), - scopes: z.array(z.string()).describe('Array of scopes granted to the token'), - note: z - .string() - .nullable() - .describe('Human-readable description of the token'), - created: z.string().describe('ISO 8601 creation timestamp'), - modified: z.string().describe('ISO 8601 last modified timestamp'), - allowedUrls: z - .array(z.string()) - .optional() - .describe('URLs that the token is restricted to'), - token: z - .string() - .optional() - .describe( - 'The actual access token string (omitted for secret tokens in list responses)' - ), - expires: z - .string() - .optional() - .describe('Expiration time in ISO 8601 format (temporary tokens only)') -}); - -export const ListTokensOutputSchema = z.object({ - tokens: z.array(TokenObjectSchema), - count: z.number().describe('Total number of tokens returned'), - next_start: z.string().optional().describe('Pagination token for next page') -}); - -export type ListTokensOutput = z.infer; -export type TokenObject = z.infer; diff --git a/src/tools/list-tokens-tool/ListTokensTool.input.schema.ts b/src/tools/list-tokens-tool/ListTokensTool.schema.ts similarity index 100% rename from src/tools/list-tokens-tool/ListTokensTool.input.schema.ts rename to src/tools/list-tokens-tool/ListTokensTool.schema.ts diff --git a/src/tools/list-tokens-tool/ListTokensTool.ts b/src/tools/list-tokens-tool/ListTokensTool.ts index 7a84d5d..5f8d6b2 100644 --- a/src/tools/list-tokens-tool/ListTokensTool.ts +++ b/src/tools/list-tokens-tool/ListTokensTool.ts @@ -1,22 +1,9 @@ -// Copyright (c) Mapbox, Inc. -// Licensed under the MIT License. - -import { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; -import type { HttpRequest } from '../../utils/types.js'; +import { fetchClient } from '../../utils/fetchRequest.js'; import { MapboxApiBasedTool } from '../MapboxApiBasedTool.js'; -import { - ListTokensSchema, - ListTokensInput -} from './ListTokensTool.input.schema.js'; -import { getUserNameFromToken } from '../../utils/jwtUtils.js'; -import { - ListTokensOutputSchema, - TokenObjectSchema -} from './ListTokensTool.output.schema.js'; +import { ListTokensSchema, ListTokensInput } from './ListTokensTool.schema.js'; export class ListTokensTool extends MapboxApiBasedTool< - typeof ListTokensSchema, - typeof ListTokensOutputSchema + typeof ListTokensSchema > { readonly name = 'list_tokens_tool'; readonly description = @@ -29,44 +16,19 @@ export class ListTokensTool extends MapboxApiBasedTool< title: 'List Mapbox Tokens Tool' }; - constructor(params: { httpRequest: HttpRequest }) { - super({ - inputSchema: ListTokensSchema, - outputSchema: ListTokensOutputSchema, - httpRequest: params.httpRequest - }); + constructor(private fetchImpl: typeof fetch = fetchClient) { + super({ inputSchema: ListTokensSchema }); } protected async execute( input: ListTokensInput, accessToken?: string - ): Promise { + ): Promise<{ type: 'text'; text: string }> { if (!accessToken) { - return { - isError: true, - content: [ - { - type: 'text', - text: 'MAPBOX_ACCESS_TOKEN is not set' - } - ] - }; + throw new Error('MAPBOX_ACCESS_TOKEN is not set'); } - let userName; - try { - userName = getUserNameFromToken(accessToken); - } catch (error) { - return { - isError: true, - content: [ - { - type: 'text', - text: `Invalid access token: ${(error as Error).message}` - } - ] - }; - } + const username = MapboxApiBasedTool.getUserNameFromToken(accessToken); this.log( 'info', @@ -94,7 +56,7 @@ export class ListTokensTool extends MapboxApiBasedTool< } let url: string | null = - `${MapboxApiBasedTool.mapboxApiEndpoint}tokens/v2/${userName}?${params.toString()}`; + `${MapboxApiBasedTool.mapboxApiEndpoint}tokens/v2/${username}?${params.toString()}`; const allTokens: unknown[] = []; let pageCount = 0; let nextPageUrl: string | null = null; @@ -110,7 +72,7 @@ export class ListTokensTool extends MapboxApiBasedTool< this.log('info', `ListTokensTool: Fetching page ${pageCount}`); this.log('debug', `ListTokensTool: Fetching URL: ${url}`); - const response = await this.httpRequest(url, { + const response = await this.fetchImpl(url, { method: 'GET', headers: { 'Content-Type': 'application/json' @@ -118,7 +80,14 @@ export class ListTokensTool extends MapboxApiBasedTool< }); if (!response.ok) { - return this.handleApiError(response, 'list tokens'); + const errorBody = await response.text(); + this.log( + 'error', + `ListTokensTool: API Error - Status: ${response.status}, Body: ${errorBody}` + ); + throw new Error( + `Failed to list tokens: ${response.status} ${response.statusText}` + ); } const data = await response.json(); @@ -128,24 +97,7 @@ export class ListTokensTool extends MapboxApiBasedTool< ? data : (data as { tokens?: unknown[] }).tokens || []; - // Validate tokens array against TokenObjectSchema - const parseResult = TokenObjectSchema.array().safeParse(tokens); - if (!parseResult.success) { - this.log( - 'error', - `ListTokensTool: Token array schema validation failed\n${parseResult.error}` - ); - return { - isError: true, - content: [ - { - type: 'text', - text: `ListTokensTool: Response does not conform to token array schema:\n${parseResult.error}` - } - ] - }; - } - allTokens.push(...parseResult.data); + allTokens.push(...tokens); this.log( 'info', `ListTokensTool: Retrieved ${tokens.length} tokens on page ${pageCount}` @@ -201,26 +153,14 @@ export class ListTokensTool extends MapboxApiBasedTool< } return { - content: [ - { - type: 'text', - text: JSON.stringify(result, null, 2) - } - ], - structuredContent: result, - isError: false + type: 'text', + text: JSON.stringify(result, null, 2) }; } catch (error) { - this.log('error', `ListTokensTool: Unexpected error: ${error}`); - return { - isError: true, - content: [ - { - type: 'text', - text: `ListTokensTool: Unexpected error: ${error}` - } - ] - }; + if (error instanceof Error) { + throw error; + } + throw new Error(`Failed to list tokens: ${String(error)}`); } } diff --git a/src/tools/preview-style-tool/PreviewStyleTool.input.schema.ts b/src/tools/preview-style-tool/PreviewStyleTool.schema.ts similarity index 100% rename from src/tools/preview-style-tool/PreviewStyleTool.input.schema.ts rename to src/tools/preview-style-tool/PreviewStyleTool.schema.ts diff --git a/src/tools/preview-style-tool/PreviewStyleTool.ts b/src/tools/preview-style-tool/PreviewStyleTool.ts index bb0adc8..852ed6c 100644 --- a/src/tools/preview-style-tool/PreviewStyleTool.ts +++ b/src/tools/preview-style-tool/PreviewStyleTool.ts @@ -1,11 +1,9 @@ -import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; import { BaseTool } from '../BaseTool.js'; import { MapboxApiBasedTool } from '../MapboxApiBasedTool.js'; import { PreviewStyleSchema, PreviewStyleInput -} from './PreviewStyleTool.input.schema.js'; -import { getUserNameFromToken } from '../../utils/jwtUtils.js'; +} from './PreviewStyleTool.schema.js'; export class PreviewStyleTool extends BaseTool { readonly name = 'preview_style_tool'; @@ -23,21 +21,10 @@ export class PreviewStyleTool extends BaseTool { super({ inputSchema: PreviewStyleSchema }); } - protected async execute(input: PreviewStyleInput): Promise { - let userName: string; - try { - userName = getUserNameFromToken(input.accessToken); - } catch (error) { - return { - isError: true, - content: [ - { - type: 'text', - text: error instanceof Error ? error.message : String(error) - } - ] - }; - } + protected async execute( + input: PreviewStyleInput + ): Promise<{ type: 'text'; text: string }> { + const username = MapboxApiBasedTool.getUserNameFromToken(input.accessToken); // Use the user-provided public token const publicToken = input.accessToken; @@ -61,16 +48,11 @@ export class PreviewStyleTool extends BaseTool { const hashFragment = hashParams.length > 0 ? `#${hashParams.join('/')}` : ''; - const url = `${MapboxApiBasedTool.mapboxApiEndpoint}styles/v1/${userName}/${input.styleId}.html?${params.toString()}${hashFragment}`; + const url = `${MapboxApiBasedTool.mapboxApiEndpoint}styles/v1/${username}/${input.styleId}.html?${params.toString()}${hashFragment}`; return { - content: [ - { - type: 'text', - text: url - } - ], - isError: false + type: 'text', + text: url }; } } diff --git a/src/tools/retrieve-style-tool/RetrieveStyleTool.output.schema.ts b/src/tools/retrieve-style-tool/RetrieveStyleTool.output.schema.ts deleted file mode 100644 index dc5f0ce..0000000 --- a/src/tools/retrieve-style-tool/RetrieveStyleTool.output.schema.ts +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) Mapbox, Inc. -// Licensed under the MIT License. - -import { z } from 'zod'; -import { BaseStylePropertiesSchema } from '../../schemas/style.js'; - -// OUTPUT Schema - For API responses (GET/PATCH response) -// TODO: Refactor into shared schema -export const MapboxStyleOutputSchema = BaseStylePropertiesSchema.extend({ - name: z.string().describe('Human-readable name for the style'), - - // API-specific properties (only present in responses) - id: z.string().describe('Unique style identifier'), - owner: z.string().describe('Username of the style owner'), - created: z - .string() - .datetime() - .describe('ISO 8601 timestamp when style was created'), - modified: z - .string() - .datetime() - .describe('ISO 8601 timestamp when style was last modified'), - visibility: z - .enum(['public', 'private']) - .describe('Style visibility setting'), - protected: z - .boolean() - .optional() - .describe('Whether style is protected from modifications'), - draft: z.boolean().optional().describe('Whether this is a draft version') -}); - -// Type exports -export type MapboxStyleOutput = z.infer; diff --git a/src/tools/retrieve-style-tool/RetrieveStyleTool.input.schema.ts b/src/tools/retrieve-style-tool/RetrieveStyleTool.schema.ts similarity index 100% rename from src/tools/retrieve-style-tool/RetrieveStyleTool.input.schema.ts rename to src/tools/retrieve-style-tool/RetrieveStyleTool.schema.ts diff --git a/src/tools/retrieve-style-tool/RetrieveStyleTool.ts b/src/tools/retrieve-style-tool/RetrieveStyleTool.ts index d8454aa..a8167da 100644 --- a/src/tools/retrieve-style-tool/RetrieveStyleTool.ts +++ b/src/tools/retrieve-style-tool/RetrieveStyleTool.ts @@ -1,23 +1,13 @@ -// Copyright (c) Mapbox, Inc. -// Licensed under the MIT License. - -import { getUserNameFromToken } from '../../utils/jwtUtils.js'; +import { fetchClient } from '../../utils/fetchRequest.js'; import { filterExpandedMapboxStyles } from '../../utils/styleUtils.js'; import { MapboxApiBasedTool } from '../MapboxApiBasedTool.js'; import { RetrieveStyleSchema, RetrieveStyleInput -} from './RetrieveStyleTool.input.schema.js'; -import { HttpRequest } from '../../utils/types.js'; -import { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; -import { - MapboxStyleOutput, - MapboxStyleOutputSchema -} from './RetrieveStyleTool.output.schema.js'; +} from './RetrieveStyleTool.schema.js'; export class RetrieveStyleTool extends MapboxApiBasedTool< - typeof RetrieveStyleSchema, - typeof MapboxStyleOutputSchema + typeof RetrieveStyleSchema > { name = 'retrieve_style_tool'; description = 'Retrieve a specific Mapbox style by ID'; @@ -29,52 +19,27 @@ export class RetrieveStyleTool extends MapboxApiBasedTool< title: 'Retrieve Mapbox Style Tool' }; - constructor(params: { httpRequest: HttpRequest }) { - super({ - inputSchema: RetrieveStyleSchema, - outputSchema: MapboxStyleOutputSchema, - httpRequest: params.httpRequest - }); + constructor(private fetch: typeof globalThis.fetch = fetchClient) { + super({ inputSchema: RetrieveStyleSchema }); } protected async execute( input: RetrieveStyleInput, accessToken?: string - ): Promise { - const username = getUserNameFromToken(accessToken); + ): Promise { + const username = MapboxApiBasedTool.getUserNameFromToken(accessToken); const url = `${MapboxApiBasedTool.mapboxApiEndpoint}styles/v1/${username}/${input.styleId}?access_token=${accessToken}`; - const response = await this.httpRequest(url); + const response = await this.fetch(url); if (!response.ok) { - return this.handleApiError(response, 'retrieve style'); - } - - const rawData = await response.json(); - // Validate response against schema with graceful fallback - let data: MapboxStyleOutput; - try { - data = MapboxStyleOutputSchema.parse(rawData); - } catch (validationError) { - this.log( - 'warning', - `Schema validation failed for search response: ${validationError instanceof Error ? validationError.message : 'Unknown validation error'}` + throw new Error( + `Failed to retrieve style: ${response.status} ${response.statusText}` ); - // Graceful fallback to raw data - data = rawData as MapboxStyleOutput; } - this.log('info', `UpdateStyleTool: Successfully updated style ${data.id}`); - - return { - content: [ - { - type: 'text', - text: JSON.stringify(filterExpandedMapboxStyles(data), null, 2) - } - ], - structuredContent: filterExpandedMapboxStyles(data), - isError: false - }; + const data = await response.json(); + // Always filter out expanded Mapbox styles to prevent token overflow + return filterExpandedMapboxStyles(data); } } diff --git a/src/tools/style-builder-tool/StyleBuilderTool.input.schema.ts b/src/tools/style-builder-tool/StyleBuilderTool.schema.ts similarity index 99% rename from src/tools/style-builder-tool/StyleBuilderTool.input.schema.ts rename to src/tools/style-builder-tool/StyleBuilderTool.schema.ts index 0d34073..256f4f6 100644 --- a/src/tools/style-builder-tool/StyleBuilderTool.input.schema.ts +++ b/src/tools/style-builder-tool/StyleBuilderTool.schema.ts @@ -1,6 +1,3 @@ -// Copyright (c) Mapbox, Inc. -// Licensed under the MIT License. - import { z } from 'zod'; const LayerConfigSchema = z.object({ diff --git a/src/tools/style-builder-tool/StyleBuilderTool.ts b/src/tools/style-builder-tool/StyleBuilderTool.ts index 0f093f5..47d3585 100644 --- a/src/tools/style-builder-tool/StyleBuilderTool.ts +++ b/src/tools/style-builder-tool/StyleBuilderTool.ts @@ -1,12 +1,8 @@ -// Copyright (c) Mapbox, Inc. -// Licensed under the MIT License. - import { BaseTool } from '../BaseTool.js'; -import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; import { StyleBuilderToolSchema, type StyleBuilderToolInput -} from './StyleBuilderTool.input.schema.js'; +} from './StyleBuilderTool.schema.js'; // Using STREETS_V8_FIELDS as single source of truth instead of MAPBOX_STYLE_LAYERS import { STREETS_V8_FIELDS } from '../../constants/mapboxStreetsV8Fields.js'; import type { Layer, Filter, MapboxStyle } from '../../types/mapbox-style.js'; @@ -56,6 +52,7 @@ const SOURCE_LAYER_GEOMETRY: Record< export class StyleBuilderTool extends BaseTool { name = 'style_builder_tool'; + private currentSourceLayer?: string; // Track current source layer for better error messages readonly annotations = { readOnlyHint: true, destructiveHint: false, @@ -129,9 +126,7 @@ If a layer type is not recognized, the tool will provide helpful suggestions sho super({ inputSchema: StyleBuilderToolSchema }); } - protected async execute( - input: StyleBuilderToolInput - ): Promise { + protected async execute(input: StyleBuilderToolInput) { try { const result = this.buildStyle(input); const { style, corrections, layerHelp, availableProperties } = result; @@ -1441,6 +1436,9 @@ ${JSON.stringify(style, null, 2)} const filters: unknown[] = []; const corrections: string[] = []; + // Set current source layer for better error messages + this.currentSourceLayer = sourceLayer; + // Get field definitions for this source layer const layerFields = STREETS_V8_FIELDS[sourceLayer as keyof typeof STREETS_V8_FIELDS]; diff --git a/src/tools/style-comparison-tool/StyleComparisonTool.schema.ts b/src/tools/style-comparison-tool/StyleComparisonTool.schema.ts index ec25e25..aa62aec 100644 --- a/src/tools/style-comparison-tool/StyleComparisonTool.schema.ts +++ b/src/tools/style-comparison-tool/StyleComparisonTool.schema.ts @@ -1,6 +1,3 @@ -// Copyright (c) Mapbox, Inc. -// Licensed under the MIT License. - import { z } from 'zod'; export const StyleComparisonSchema = z.object({ diff --git a/src/tools/style-comparison-tool/StyleComparisonTool.ts b/src/tools/style-comparison-tool/StyleComparisonTool.ts index 1bfb6ac..2aad35f 100644 --- a/src/tools/style-comparison-tool/StyleComparisonTool.ts +++ b/src/tools/style-comparison-tool/StyleComparisonTool.ts @@ -1,13 +1,9 @@ -// Copyright (c) Mapbox, Inc. -// Licensed under the MIT License. - -import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; import { BaseTool } from '../BaseTool.js'; +import { MapboxApiBasedTool } from '../MapboxApiBasedTool.js'; import { StyleComparisonSchema, StyleComparisonInput } from './StyleComparisonTool.schema.js'; -import { getUserNameFromToken } from '../../utils/jwtUtils.js'; export class StyleComparisonTool extends BaseTool< typeof StyleComparisonSchema @@ -43,7 +39,7 @@ export class StyleComparisonTool extends BaseTool< // If it's just a style ID, try to get username from the token try { - const username = getUserNameFromToken(accessToken); + const username = MapboxApiBasedTool.getUserNameFromToken(accessToken); return `${username}/${style}`; } catch (error) { throw new Error( @@ -58,27 +54,10 @@ export class StyleComparisonTool extends BaseTool< protected async execute( input: StyleComparisonInput - ): Promise { - let beforeStyleId; - let afterStyleId; - try { - // Process style IDs to get username/styleId format - beforeStyleId = this.processStyleId(input.before, input.accessToken); - afterStyleId = this.processStyleId(input.after, input.accessToken); - } catch (error) { - return { - content: [ - { - type: 'text', - text: - error instanceof Error - ? error.message - : 'An unknown error occurred' - } - ], - isError: true - }; - } + ): Promise<{ type: 'text'; text: string }> { + // Process style IDs to get username/styleId format + const beforeStyleId = this.processStyleId(input.before, input.accessToken); + const afterStyleId = this.processStyleId(input.after, input.accessToken); // Build the comparison URL const params = new URLSearchParams(); @@ -100,13 +79,8 @@ export class StyleComparisonTool extends BaseTool< } return { - content: [ - { - type: 'text', - text: url - } - ], - isError: false + type: 'text', + text: url }; } } diff --git a/src/tools/tilequery-tool/TilequeryTool.output.schema.ts b/src/tools/tilequery-tool/TilequeryTool.output.schema.ts deleted file mode 100644 index d4b3c86..0000000 --- a/src/tools/tilequery-tool/TilequeryTool.output.schema.ts +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright (c) Mapbox, Inc. -// Licensed under the MIT License. - -import { z } from 'zod'; - -// Coordinate pair schema -const CoordinatesSchema = z.tuple([z.number(), z.number()]); - -// Vector Tileset Feature Schema -const VectorTilequeryFeatureSchema = z.object({ - type: z.literal('Feature'), - id: z.string().describe('Feature identifier'), - geometry: z.object({ - type: z.literal('Point'), - coordinates: CoordinatesSchema - }), - properties: z - .object({ - tilequery: z.object({ - distance: z - .number() - .describe( - 'Approximate surface distance from feature to queried point, in meters' - ), - geometry: z - .enum(['point', 'linestring', 'polygon']) - .describe('Original geometry type of the feature'), - layer: z - .string() - .describe('The vector tile layer of the feature result') - }) - }) - .and(z.record(z.any())) // Allow additional properties from the original feature -}); - -// Rasterarray Tileset Feature Schema -const RasterarrayTilequeryFeatureSchema = z.object({ - type: z.literal('Feature'), - id: z.null(), - geometry: z.object({ - type: z.literal('Point'), - coordinates: CoordinatesSchema - }), - properties: z.object({ - tilequery: z.object({ - layer: z.string().describe('The layer that the feature belongs to'), - band: z.string().describe('The band that the feature belongs to'), - zoom: z - .number() - .describe('The maxzoom level at which the point value was extracted'), - units: z.string().describe('The unit of measurement for the point value') - }), - val: z - .union([z.number(), z.array(z.number())]) - .describe( - 'Point value at the requested location (number for single-band, array for multi-dimensional data)' - ) - }) -}); - -// Union of both feature types -const TilequeryFeatureSchema = z.union([ - VectorTilequeryFeatureSchema, - RasterarrayTilequeryFeatureSchema -]); - -// Main Tilequery Response Schema -export const TilequeryResponseSchema = z.object({ - type: z.literal('FeatureCollection'), - features: z.array(TilequeryFeatureSchema) -}); - -// Type exports inferred from Zod schemas -export type VectorTilequeryFeature = z.infer< - typeof VectorTilequeryFeatureSchema ->; -export type RasterarrayTilequeryFeature = z.infer< - typeof RasterarrayTilequeryFeatureSchema ->; -export type TilequeryFeature = z.infer; -export type TilequeryResponse = z.infer; diff --git a/src/tools/tilequery-tool/TilequeryTool.input.schema.ts b/src/tools/tilequery-tool/TilequeryTool.schema.ts similarity index 95% rename from src/tools/tilequery-tool/TilequeryTool.input.schema.ts rename to src/tools/tilequery-tool/TilequeryTool.schema.ts index 1808987..b50a264 100644 --- a/src/tools/tilequery-tool/TilequeryTool.input.schema.ts +++ b/src/tools/tilequery-tool/TilequeryTool.schema.ts @@ -1,6 +1,3 @@ -// Copyright (c) Mapbox, Inc. -// Licensed under the MIT License. - import { z } from 'zod'; export const TilequerySchema = z.object({ diff --git a/src/tools/tilequery-tool/TilequeryTool.ts b/src/tools/tilequery-tool/TilequeryTool.ts index 3737293..f5e75ca 100644 --- a/src/tools/tilequery-tool/TilequeryTool.ts +++ b/src/tools/tilequery-tool/TilequeryTool.ts @@ -1,22 +1,8 @@ -// Copyright (c) Mapbox, Inc. -// Licensed under the MIT License. - -import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; -import type { HttpRequest } from '../../utils/types.js'; +import { fetchClient } from '../../utils/fetchRequest.js'; import { MapboxApiBasedTool } from '../MapboxApiBasedTool.js'; -import { - TilequerySchema, - TilequeryInput -} from './TilequeryTool.input.schema.js'; -import { - TilequeryResponse, - TilequeryResponseSchema -} from './TilequeryTool.output.schema.js'; +import { TilequerySchema, TilequeryInput } from './TilequeryTool.schema.js'; -export class TilequeryTool extends MapboxApiBasedTool< - typeof TilequerySchema, - typeof TilequeryResponseSchema -> { +export class TilequeryTool extends MapboxApiBasedTool { name = 'tilequery_tool'; description = 'Query vector and raster data from Mapbox tilesets at geographic coordinates'; @@ -28,18 +14,14 @@ export class TilequeryTool extends MapboxApiBasedTool< title: 'Mapbox Tilequery Tool' }; - constructor(params: { httpRequest: HttpRequest }) { - super({ - inputSchema: TilequerySchema, - outputSchema: TilequeryResponseSchema, - httpRequest: params.httpRequest - }); + constructor(private fetch: typeof globalThis.fetch = fetchClient) { + super({ inputSchema: TilequerySchema }); } protected async execute( input: TilequeryInput, accessToken?: string - ): Promise { + ): Promise { const { tilesetId, longitude, latitude, ...queryParams } = input; const url = new URL( `${MapboxApiBasedTool.mapboxApiEndpoint}v4/${tilesetId}/tilequery/${longitude},${latitude}.json` @@ -71,41 +53,16 @@ export class TilequeryTool extends MapboxApiBasedTool< url.searchParams.set('access_token', accessToken || ''); - const response = await this.httpRequest(url.toString()); + const response = await this.fetch(url.toString()); if (!response.ok) { - return this.handleApiError(response, 'query tile'); - } - - const rawData = await response.json(); - - // Validate response against schema with graceful fallback - let data: TilequeryResponse; - try { - data = TilequeryResponseSchema.parse(rawData); - } catch (validationError) { - this.log( - 'warning', - `Schema validation failed for search response: ${validationError instanceof Error ? validationError.message : 'Unknown validation error'}` + const errorText = await response.text(); + throw new Error( + `Tilequery request failed: ${response.status} ${response.statusText}. ${errorText}` ); - // Graceful fallback to raw data - data = rawData as TilequeryResponse; } - this.log( - 'info', - `TilequeryTool: Successfully completed query, found ${data.features?.length || 0} results` - ); - - return { - content: [ - { - type: 'text', - text: JSON.stringify(data, null, 2) - } - ], - structuredContent: data, - isError: false - }; + const data = await response.json(); + return data; } } diff --git a/src/tools/toolRegistry.ts b/src/tools/toolRegistry.ts index fdfd910..68666ea 100644 --- a/src/tools/toolRegistry.ts +++ b/src/tools/toolRegistry.ts @@ -1,6 +1,3 @@ -// Copyright (c) Mapbox, Inc. -// Licensed under the MIT License. - import { BoundingBoxTool } from './bounding-box-tool/BoundingBoxTool.js'; import { CountryBoundingBoxTool } from './bounding-box-tool/CountryBoundingBoxTool.js'; import { CoordinateConversionTool } from './coordinate-conversion-tool/CoordinateConversionTool.js'; @@ -17,26 +14,25 @@ import { StyleBuilderTool } from './style-builder-tool/StyleBuilderTool.js'; import { StyleComparisonTool } from './style-comparison-tool/StyleComparisonTool.js'; import { TilequeryTool } from './tilequery-tool/TilequeryTool.js'; import { UpdateStyleTool } from './update-style-tool/UpdateStyleTool.js'; -import { httpRequest } from '../utils/httpPipeline.js'; // Central registry of all tools export const ALL_TOOLS = [ - new ListStylesTool({ httpRequest }), - new CreateStyleTool({ httpRequest }), - new RetrieveStyleTool({ httpRequest }), - new UpdateStyleTool({ httpRequest }), - new DeleteStyleTool({ httpRequest }), + new ListStylesTool(), + new CreateStyleTool(), + new RetrieveStyleTool(), + new UpdateStyleTool(), + new DeleteStyleTool(), new PreviewStyleTool(), new StyleBuilderTool(), new GeojsonPreviewTool(), - new CreateTokenTool({ httpRequest }), - new ListTokensTool({ httpRequest }), + new CreateTokenTool(), + new ListTokensTool(), new BoundingBoxTool(), new CountryBoundingBoxTool(), new CoordinateConversionTool(), - new GetMapboxDocSourceTool({ httpRequest }), + new GetMapboxDocSourceTool(), new StyleComparisonTool(), - new TilequeryTool({ httpRequest }) + new TilequeryTool() ] as const; export type ToolInstance = (typeof ALL_TOOLS)[number]; diff --git a/src/tools/update-style-tool/UpdateStyleTool.input.schema.ts b/src/tools/update-style-tool/UpdateStyleTool.input.schema.ts deleted file mode 100644 index 3638cdc..0000000 --- a/src/tools/update-style-tool/UpdateStyleTool.input.schema.ts +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) Mapbox, Inc. -// Licensed under the MIT License. - -import { z } from 'zod'; -import { BaseStylePropertiesSchema } from '../../schemas/style.js'; - -// INPUT Schema - For creating/updating styles (PATCH/POST request body) -export const MapboxStyleInputSchema = BaseStylePropertiesSchema.extend({ - name: z - .string() - .describe('Human-readable name for the style (REQUIRED for updates)') - .optional() - // These fields should NOT be included in input - they're read-only - // If present, they'll be ignored or cause API errors -}).passthrough(); - -export const UpdateStyleInputSchema = z.object({ - styleId: z.string().describe('Style ID to update'), - name: z.string().optional().describe('New name for the style'), - style: MapboxStyleInputSchema.optional().describe( - 'Updated Mapbox style specification object' - ) -}); - -export type UpdateStyleInput = z.infer; diff --git a/src/tools/update-style-tool/UpdateStyleTool.output.schema.ts b/src/tools/update-style-tool/UpdateStyleTool.output.schema.ts deleted file mode 100644 index 9cab567..0000000 --- a/src/tools/update-style-tool/UpdateStyleTool.output.schema.ts +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) Mapbox, Inc. -// Licensed under the MIT License. - -import { z } from 'zod'; -import { BaseStylePropertiesSchema } from '../../schemas/style.js'; - -// OUTPUT Schema - For API responses (GET/PATCH response) -export const MapboxStyleOutputSchema = BaseStylePropertiesSchema.extend({ - name: z.string().describe('Human-readable name for the style'), - - // API-specific properties (only present in responses) - id: z.string().describe('Unique style identifier'), - owner: z.string().describe('Username of the style owner'), - created: z - .string() - .datetime() - .describe('ISO 8601 timestamp when style was created'), - modified: z - .string() - .datetime() - .describe('ISO 8601 timestamp when style was last modified'), - visibility: z - .enum(['public', 'private']) - .describe('Style visibility setting'), - draft: z.boolean().optional().describe('Whether this is a draft version') -}).passthrough(); - -// Type exports -export type MapboxStyleOutput = z.infer; diff --git a/src/tools/update-style-tool/UpdateStyleTool.schema.ts b/src/tools/update-style-tool/UpdateStyleTool.schema.ts new file mode 100644 index 0000000..08d8229 --- /dev/null +++ b/src/tools/update-style-tool/UpdateStyleTool.schema.ts @@ -0,0 +1,12 @@ +import { z } from 'zod'; + +export const UpdateStyleSchema = z.object({ + styleId: z.string().describe('Style ID to update'), + name: z.string().optional().describe('New name for the style'), + style: z + .record(z.any()) + .optional() + .describe('Updated Mapbox style specification object') +}); + +export type UpdateStyleInput = z.infer; diff --git a/src/tools/update-style-tool/UpdateStyleTool.ts b/src/tools/update-style-tool/UpdateStyleTool.ts index 7f96023..b487322 100644 --- a/src/tools/update-style-tool/UpdateStyleTool.ts +++ b/src/tools/update-style-tool/UpdateStyleTool.ts @@ -1,23 +1,13 @@ -// Copyright (c) Mapbox, Inc. -// Licensed under the MIT License. - -import type { HttpRequest } from '../../utils/types.js'; +import { fetchClient } from '../../utils/fetchRequest.js'; import { filterExpandedMapboxStyles } from '../../utils/styleUtils.js'; import { MapboxApiBasedTool } from '../MapboxApiBasedTool.js'; import { - UpdateStyleInput, - UpdateStyleInputSchema -} from './UpdateStyleTool.input.schema.js'; -import { getUserNameFromToken } from '../../utils/jwtUtils.js'; -import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; -import { - MapboxStyleOutputSchema, - MapboxStyleOutput -} from './UpdateStyleTool.output.schema.js'; + UpdateStyleSchema, + UpdateStyleInput +} from './UpdateStyleTool.schema.js'; export class UpdateStyleTool extends MapboxApiBasedTool< - typeof UpdateStyleInputSchema, - typeof MapboxStyleOutputSchema + typeof UpdateStyleSchema > { name = 'update_style_tool'; description = 'Update an existing Mapbox style'; @@ -29,26 +19,22 @@ export class UpdateStyleTool extends MapboxApiBasedTool< title: 'Update Mapbox Style Tool' }; - constructor(params: { httpRequest: HttpRequest }) { - super({ - inputSchema: UpdateStyleInputSchema, - outputSchema: MapboxStyleOutputSchema, - httpRequest: params.httpRequest - }); + constructor(private fetch: typeof globalThis.fetch = fetchClient) { + super({ inputSchema: UpdateStyleSchema }); } protected async execute( input: UpdateStyleInput, accessToken?: string - ): Promise { - const username = getUserNameFromToken(accessToken); + ): Promise { + const username = MapboxApiBasedTool.getUserNameFromToken(accessToken); const url = `${MapboxApiBasedTool.mapboxApiEndpoint}styles/v1/${username}/${input.styleId}?access_token=${accessToken}`; - const payload: Record = {}; + const payload: any = {}; if (input.name) payload.name = input.name; if (input.style) Object.assign(payload, input.style); - const response = await this.httpRequest(url, { + const response = await this.fetch(url, { method: 'PATCH', headers: { 'Content-Type': 'application/json' @@ -57,34 +43,13 @@ export class UpdateStyleTool extends MapboxApiBasedTool< }); if (!response.ok) { - return this.handleApiError(response, 'update style'); - } - - const rawData = await response.json(); - // Validate response against schema with graceful fallback - let data: MapboxStyleOutput; - try { - data = MapboxStyleOutputSchema.parse(rawData); - } catch (validationError) { - this.log( - 'warning', - `Schema validation failed for search response: ${validationError instanceof Error ? validationError.message : 'Unknown validation error'}` + throw new Error( + `Failed to update style: ${response.status} ${response.statusText}` ); - // Graceful fallback to raw data - data = rawData as MapboxStyleOutput; } - this.log('info', `UpdateStyleTool: Successfully updated style ${data.id}`); - - return { - content: [ - { - type: 'text', - text: JSON.stringify(filterExpandedMapboxStyles(data), null, 2) - } - ], - structuredContent: filterExpandedMapboxStyles(data), - isError: false - }; + const data = await response.json(); + // Return full style but filter out expanded Mapbox styles + return filterExpandedMapboxStyles(data); } } diff --git a/src/utils/httpPipeline.ts b/src/utils/fetchRequest.ts similarity index 69% rename from src/utils/httpPipeline.ts rename to src/utils/fetchRequest.ts index 22afc0e..74981dc 100644 --- a/src/utils/httpPipeline.ts +++ b/src/utils/fetchRequest.ts @@ -1,35 +1,27 @@ -// Copyright (c) Mapbox, Inc. -// Licensed under the MIT License. - import { getVersionInfo } from './versionUtils.js'; -import { type HttpRequest } from './types.js'; - -function createRandomId(prefix: string): string { - return `${prefix}${Date.now()}-${Math.random().toString(36).substring(2, 11)}`; -} -export interface HttpPolicy { +export interface FetchPolicy { id: string; handle( input: string | URL | Request, init: RequestInit, - next: HttpRequest + next: typeof fetch ): Promise; } -export class HttpPipeline { - private policies: HttpPolicy[] = []; - private httpRequestImpl: HttpRequest; +export class PolicyPipeline { + private policies: FetchPolicy[] = []; + private fetchImpl: typeof fetch; - constructor(httpRequestImpl?: HttpRequest) { - this.httpRequestImpl = httpRequestImpl ?? fetch; + constructor(fetchImpl?: typeof fetch) { + this.fetchImpl = fetchImpl ?? fetch; } - usePolicy(policy: HttpPolicy) { + usePolicy(policy: FetchPolicy) { this.policies.push(policy); } - removePolicy(policyOrId: HttpPolicy | string) { + removePolicy(policyOrId: FetchPolicy | string) { if (typeof policyOrId === 'string') { this.policies = this.policies.filter((p) => p.id !== policyOrId); } else { @@ -37,7 +29,7 @@ export class HttpPipeline { } } - findPolicyById(id: string): HttpPolicy | undefined { + findPolicyById(id: string): FetchPolicy | undefined { return this.policies.find((p) => p.id === id); } @@ -45,7 +37,7 @@ export class HttpPipeline { return this.policies; } - async execute( + async fetch( input: string | URL | Request, init: RequestInit = {} ): Promise { @@ -55,32 +47,31 @@ export class HttpPipeline { options: RequestInit ): Promise => { if (i < this.policies.length) { - return this.policies[i].handle( - req, - options, - (nextReq: string | URL | Request, nextOptions?: RequestInit) => - dispatch(i + 1, nextReq, nextOptions || {}) + return this.policies[i].handle(req, options, (nextReq, nextOptions) => + dispatch(i + 1, nextReq, nextOptions!) ); } - return this.httpRequestImpl(req, options); // Use injected httpRequest + return this.fetchImpl(req, options); // Use injected fetch }; return dispatch(0, input, init); } } -export class UserAgentPolicy implements HttpPolicy { +export class UserAgentPolicy implements FetchPolicy { id: string; constructor( private userAgent: string, id?: string ) { - this.id = id ?? createRandomId('user-agent-'); + this.id = + id ?? + `user-agent-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`; } async handle( input: string | URL | Request, init: RequestInit, - next: HttpRequest + next: typeof fetch ): Promise { let headers: Headers | Record; @@ -115,7 +106,7 @@ export class UserAgentPolicy implements HttpPolicy { } } -export class RetryPolicy implements HttpPolicy { +export class RetryPolicy implements FetchPolicy { id: string; constructor( @@ -124,13 +115,15 @@ export class RetryPolicy implements HttpPolicy { private maxDelayMs: number = 2000, id?: string ) { - this.id = id ?? createRandomId('retry-'); + this.id = + id ?? + `retry-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`; } async handle( input: string | URL | Request, init: RequestInit, - next: HttpRequest + next: typeof fetch ): Promise { let attempt = 0; let lastError: Response | undefined; @@ -160,12 +153,12 @@ export class RetryPolicy implements HttpPolicy { } } -const pipeline = new HttpPipeline(); +const pipeline = new PolicyPipeline(); const versionInfo = getVersionInfo(); pipeline.usePolicy( UserAgentPolicy.fromVersionInfo(versionInfo, 'system-user-agent-policy') ); pipeline.usePolicy(new RetryPolicy(3, 200, 2000, 'system-retry-policy')); -export const httpRequest = pipeline.execute.bind(pipeline); -export const systemHttpPipeline = pipeline; +export const fetchClient = pipeline.fetch.bind(pipeline); +export const systemFetchPipeline = pipeline; diff --git a/src/utils/jwtUtils.ts b/src/utils/jwtUtils.ts deleted file mode 100644 index 9a3d3e6..0000000 --- a/src/utils/jwtUtils.ts +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright (c) Mapbox, Inc. -// Licensed under the MIT License. - -import process from 'node:process'; - -export function mapboxAccessToken() { - return process.env.MAPBOX_ACCESS_TOKEN; -} - -export function mapboxApiEndpoint() { - return process.env.MAPBOX_API_ENDPOINT || 'https://api.mapbox.com/'; -} - -/** - * Extracts the username from the Mapbox access token. - * Mapbox tokens are JWT tokens where the payload contains the username. - * @throws Error if the token is not set, invalid, or doesn't contain username - */ -export function getUserNameFromToken(accessToken?: string): string { - const token = accessToken || mapboxAccessToken(); - if (!token) { - throw new Error( - 'No access token provided. Please set MAPBOX_ACCESS_TOKEN environment variable or pass it as an argument.' - ); - } - - try { - // JWT format: header.payload.signature - const parts = token.split('.'); - if (parts.length !== 3) { - throw new Error('MAPBOX_ACCESS_TOKEN is not in valid JWT format'); - } - - // Decode the payload (second part) - const payload = JSON.parse( - Buffer.from(parts[1], 'base64').toString('utf-8') - ); - - // The username is stored in the 'u' field - if (!payload.u) { - throw new Error( - 'MAPBOX_ACCESS_TOKEN does not contain username in payload' - ); - } - - return payload.u; - } catch (error) { - if (error instanceof Error) { - throw error; - } - throw new Error('Failed to parse MAPBOX_ACCESS_TOKEN'); - } -} diff --git a/src/utils/styleUtils.ts b/src/utils/styleUtils.ts index a88de18..2a8cd40 100644 --- a/src/utils/styleUtils.ts +++ b/src/utils/styleUtils.ts @@ -1,6 +1,3 @@ -// Copyright (c) Mapbox, Inc. -// Licensed under the MIT License. - /** * Filters out expanded Mapbox styles from imports to reduce response size. * This preserves the reference to the style (e.g., mapbox://styles/mapbox/standard) diff --git a/src/utils/types.ts b/src/utils/types.ts deleted file mode 100644 index 689240f..0000000 --- a/src/utils/types.ts +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) Mapbox, Inc. -// Licensed under the MIT License. - -/** - * HttpRequest interface that includes tracing information - */ -export interface HttpRequest { - (input: string | URL | Request, init?: RequestInit): Promise; -} diff --git a/src/utils/versionUtils-cjs.cts b/src/utils/versionUtils-cjs.cts index 4e3bf55..e8c9d34 100644 --- a/src/utils/versionUtils-cjs.cts +++ b/src/utils/versionUtils-cjs.cts @@ -1,6 +1,3 @@ -// Copyright (c) Mapbox, Inc. -// Licensed under the MIT License. - import { readFileSync } from 'node:fs'; import path from 'node:path'; diff --git a/src/utils/versionUtils.ts b/src/utils/versionUtils.ts index c628473..44779ea 100644 --- a/src/utils/versionUtils.ts +++ b/src/utils/versionUtils.ts @@ -1,6 +1,3 @@ -// Copyright (c) Mapbox, Inc. -// Licensed under the MIT License. - import { readFileSync } from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; diff --git a/test/tools/MapboxApiBasedTool.test.ts b/test/tools/MapboxApiBasedTool.test.ts index c877770..ff6ed04 100644 --- a/test/tools/MapboxApiBasedTool.test.ts +++ b/test/tools/MapboxApiBasedTool.test.ts @@ -7,20 +7,9 @@ process.env.MAPBOX_ACCESS_TOKEN = `eyJhbGciOiJIUzI1NiJ9.${payload}.signature`; import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { z } from 'zod'; import { MapboxApiBasedTool } from '../../src/tools/MapboxApiBasedTool.js'; -import { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; -import type { HttpRequest } from '../../src/utils/types.js'; -import { setupHttpRequest } from '../utils/httpPipelineUtils.js'; // Create a minimal implementation of MapboxApiBasedTool for testing class TestTool extends MapboxApiBasedTool { - // Provide minimal but realistic annotations for the test tool - annotations = { - title: 'Test Tool', - readOnlyHint: true, - idempotentHint: true, - openWorldHint: false, - destructiveHint: false - }; readonly name = 'test_tool'; readonly description = 'Tool for testing MapboxApiBasedTool error handling'; @@ -28,16 +17,13 @@ class TestTool extends MapboxApiBasedTool { testParam: z.string() }); - constructor(params: { httpRequest: HttpRequest }) { - super({ - inputSchema: TestTool.inputSchema, - httpRequest: params.httpRequest - }); + constructor() { + super({ inputSchema: TestTool.inputSchema }); } protected async execute( _input: z.infer - ): Promise { + ): Promise { throw new Error('Test error message'); } } @@ -57,8 +43,7 @@ describe('MapboxApiBasedTool', () => { configurable: true }); - const { httpRequest } = setupHttpRequest(); - testTool = new TestTool({ httpRequest }); + testTool = new TestTool(); // Mock the log method to test that errors are properly logged testTool['log'] = vi.fn(); }); @@ -70,6 +55,62 @@ describe('MapboxApiBasedTool', () => { vi.unstubAllEnvs(); }); + describe('getUserNameFromToken', () => { + it('extracts username from valid token', () => { + const testPayload = Buffer.from( + JSON.stringify({ u: 'myusername' }) + ).toString('base64'); + const spy = vi + .spyOn(MapboxApiBasedTool, 'mapboxAccessToken', 'get') + .mockReturnValue(`eyJhbGciOiJIUzI1NiJ9.${testPayload}.signature`); + + const username = MapboxApiBasedTool.getUserNameFromToken(); + expect(username).toBe('myusername'); + + spy.mockRestore(); + }); + + it('throws error when token is not set', () => { + const spy = vi + .spyOn(MapboxApiBasedTool, 'mapboxAccessToken', 'get') + .mockReturnValue(undefined); + + expect(() => MapboxApiBasedTool.getUserNameFromToken()).toThrow( + 'No access token provided. Please set MAPBOX_ACCESS_TOKEN environment variable or pass it as an argument.' + ); + + spy.mockRestore(); + }); + + it('throws error when token has invalid format', () => { + const spy = vi + .spyOn(MapboxApiBasedTool, 'mapboxAccessToken', 'get') + .mockReturnValue('invalid-token-format'); + + expect(() => MapboxApiBasedTool.getUserNameFromToken()).toThrow( + 'MAPBOX_ACCESS_TOKEN is not in valid JWT format' + ); + + spy.mockRestore(); + }); + + it('throws error when payload does not contain username', () => { + const invalidPayload = Buffer.from( + JSON.stringify({ sub: 'test' }) + ).toString('base64'); + + const spy = vi + .spyOn(MapboxApiBasedTool, 'mapboxAccessToken', 'get') + .mockReturnValue(`eyJhbGciOiJIUzI1NiJ9.${invalidPayload}.signature`); + + expect(() => MapboxApiBasedTool.getUserNameFromToken()).toThrow( + 'MAPBOX_ACCESS_TOKEN does not contain username in payload' + ); + + spy.mockRestore(); + }); + }); + describe('JWT token validation', () => { it('throws an error when the token is not in a valid JWT format', async () => { const spy = vi @@ -77,8 +118,7 @@ describe('MapboxApiBasedTool', () => { .mockReturnValue('invalid-token-format'); // Create a new instance with the modified token - const { httpRequest } = setupHttpRequest(); - const toolWithInvalidToken = new TestTool({ httpRequest }); + const toolWithInvalidToken = new TestTool(); // Mock the log method separately for this instance toolWithInvalidToken['log'] = vi.fn(); @@ -112,10 +152,7 @@ describe('MapboxApiBasedTool', () => { process.env.MAPBOX_ACCESS_TOKEN = `eyJhbGciOiJIUzI1NiJ9.${validPayload}.signature`; // Override execute to return a success result instead of throwing an error - testTool['execute'] = vi.fn().mockResolvedValue({ - content: [{ type: 'text', text: JSON.stringify({ success: true }) }], - isError: false - }); + testTool['execute'] = vi.fn().mockResolvedValue({ success: true }); const result = await testTool.run({ testParam: 'test' }); @@ -128,6 +165,35 @@ describe('MapboxApiBasedTool', () => { }); }); + describe('username extraction from token', () => { + it('throws error for invalid JWT format', () => { + const spy = vi + .spyOn(MapboxApiBasedTool, 'mapboxAccessToken', 'get') + .mockReturnValue('invalid-token'); + + expect(() => { + MapboxApiBasedTool.getUserNameFromToken(); + }).toThrow('MAPBOX_ACCESS_TOKEN is not in valid JWT format'); + + spy.mockRestore(); + }); + + it('throws error when username field is missing', () => { + const tokenWithoutUsername = + 'eyJhbGciOiJIUzI1NiJ9.eyJhIjoidGVzdC1hcGkifQ.signature'; + + const spy = vi + .spyOn(MapboxApiBasedTool, 'mapboxAccessToken', 'get') + .mockReturnValue(tokenWithoutUsername); + + expect(() => { + MapboxApiBasedTool.getUserNameFromToken(); + }).toThrow('MAPBOX_ACCESS_TOKEN does not contain username in payload'); + + spy.mockRestore(); + }); + }); + describe('error handling', () => { it('returns generic error message when VERBOSE_ERRORS is not set to true', async () => { // Make sure VERBOSE_ERRORS is not set to true diff --git a/test/tools/bounding-box-tool/BoundingBoxTool.test.ts b/test/tools/bounding-box-tool/BoundingBoxTool.test.ts index 747e4f1..85f130a 100644 --- a/test/tools/bounding-box-tool/BoundingBoxTool.test.ts +++ b/test/tools/bounding-box-tool/BoundingBoxTool.test.ts @@ -1,6 +1,3 @@ -// Copyright (c) Mapbox, Inc. -// Licensed under the MIT License. - import { describe, it, expect, beforeEach } from 'vitest'; import { BoundingBoxTool } from '../../../src/tools/bounding-box-tool/BoundingBoxTool.js'; @@ -25,9 +22,9 @@ describe('BoundingBoxTool', () => { expect(result.isError).toBe(false); expect(result.content[0]).toHaveProperty('type', 'text'); const textContent = result.content[0] as TextContent; - expect(JSON.parse(textContent.text)).toEqual({ - bbox: [-73.9857, 40.7484, -73.9857, 40.7484] - }); + expect(JSON.parse(textContent.text)).toEqual([ + -73.9857, 40.7484, -73.9857, 40.7484 + ]); }); it('should calculate bounding box for a Point with string input', async () => { @@ -41,9 +38,9 @@ describe('BoundingBoxTool', () => { expect(result.isError).toBe(false); expect(result.content[0]).toHaveProperty('type', 'text'); const textContent = result.content[0] as TextContent; - expect(JSON.parse(textContent.text)).toEqual({ - bbox: [-73.9857, 40.7484, -73.9857, 40.7484] - }); + expect(JSON.parse(textContent.text)).toEqual([ + -73.9857, 40.7484, -73.9857, 40.7484 + ]); }); it('should calculate bounding box for a LineString', async () => { @@ -61,9 +58,9 @@ describe('BoundingBoxTool', () => { expect(result.isError).toBe(false); expect(result.content[0]).toHaveProperty('type', 'text'); const textContent = result.content[0] as TextContent; - expect(JSON.parse(textContent.text)).toEqual({ - bbox: [-73.9919, 40.7484, -73.9857, 40.7614] - }); + expect(JSON.parse(textContent.text)).toEqual([ + -73.9919, 40.7484, -73.9857, 40.7614 + ]); }); it('should calculate bounding box for a Polygon', async () => { @@ -85,9 +82,9 @@ describe('BoundingBoxTool', () => { expect(result.isError).toBe(false); expect(result.content[0]).toHaveProperty('type', 'text'); const textContent = result.content[0] as TextContent; - expect(JSON.parse(textContent.text)).toEqual({ - bbox: [-73.9919, 40.7484, -73.9857, 40.7614] - }); + expect(JSON.parse(textContent.text)).toEqual([ + -73.9919, 40.7484, -73.9857, 40.7614 + ]); }); it('should calculate bounding box for a FeatureCollection', async () => { @@ -118,9 +115,9 @@ describe('BoundingBoxTool', () => { expect(result.isError).toBe(false); expect(result.content[0]).toHaveProperty('type', 'text'); const textContent = result.content[0] as TextContent; - expect(JSON.parse(textContent.text)).toEqual({ - bbox: [-74.006, 40.7128, -73.9857, 40.7484] - }); + expect(JSON.parse(textContent.text)).toEqual([ + -74.006, 40.7128, -73.9857, 40.7484 + ]); }); it('should calculate bounding box for a MultiPoint', async () => { @@ -138,9 +135,9 @@ describe('BoundingBoxTool', () => { expect(result.isError).toBe(false); expect(result.content[0]).toHaveProperty('type', 'text'); const textContent = result.content[0] as TextContent; - expect(JSON.parse(textContent.text)).toEqual({ - bbox: [-74.006, 40.7128, -73.9352, 40.7484] - }); + expect(JSON.parse(textContent.text)).toEqual([ + -74.006, 40.7128, -73.9352, 40.7484 + ]); }); it('should calculate bounding box for a MultiPolygon', async () => { @@ -173,9 +170,7 @@ describe('BoundingBoxTool', () => { expect(result.isError).toBe(false); expect(result.content[0]).toHaveProperty('type', 'text'); const textContent = result.content[0] as TextContent; - expect(JSON.parse(textContent.text)).toEqual({ - bbox: [0, 0, 3, 3] - }); + expect(JSON.parse(textContent.text)).toEqual([0, 0, 3, 3]); }); it('should calculate bounding box for a GeometryCollection', async () => { @@ -201,9 +196,9 @@ describe('BoundingBoxTool', () => { expect(result.isError).toBe(false); expect(result.content[0]).toHaveProperty('type', 'text'); const textContent = result.content[0] as TextContent; - expect(JSON.parse(textContent.text)).toEqual({ - bbox: [-74.006, 40.7128, -73.9352, 40.7484] - }); + expect(JSON.parse(textContent.text)).toEqual([ + -74.006, 40.7128, -73.9352, 40.7484 + ]); }); it('should handle Feature with null geometry', async () => { @@ -273,7 +268,7 @@ describe('BoundingBoxTool', () => { it('should have correct input schema', async () => { const { BoundingBoxSchema } = await import( - '../../../src/tools/bounding-box-tool/BoundingBoxTool.input.schema.js' + '../../../src/tools/bounding-box-tool/BoundingBoxTool.schema.js' ); expect(BoundingBoxSchema).toBeDefined(); expect(BoundingBoxSchema.shape.geojson).toBeDefined(); diff --git a/test/tools/bounding-box-tool/CountryBoundingBoxTool.test.ts b/test/tools/bounding-box-tool/CountryBoundingBoxTool.test.ts index 1245d74..8d8d197 100644 --- a/test/tools/bounding-box-tool/CountryBoundingBoxTool.test.ts +++ b/test/tools/bounding-box-tool/CountryBoundingBoxTool.test.ts @@ -1,6 +1,3 @@ -// Copyright (c) Mapbox, Inc. -// Licensed under the MIT License. - import { describe, it, expect, beforeEach } from 'vitest'; import { CountryBoundingBoxTool } from '../../../src/tools/bounding-box-tool/CountryBoundingBoxTool.js'; @@ -21,9 +18,7 @@ describe('CountryBoundingBoxTool', () => { expect(result.content[0]).toHaveProperty('type', 'text'); const textContent = result.content[0] as TextContent; const bbox = JSON.parse(textContent.text); - expect(bbox).toEqual({ - bbox: [73.599819, 21.144707, 134.762115, 53.424591] - }); + expect(bbox).toEqual([73.599819, 21.144707, 134.762115, 53.424591]); }); it('should return bounding box for valid country code - United States (US)', async () => { @@ -33,9 +28,7 @@ describe('CountryBoundingBoxTool', () => { expect(result.content[0]).toHaveProperty('type', 'text'); const textContent = result.content[0] as TextContent; const bbox = JSON.parse(textContent.text); - expect(bbox).toEqual({ - bbox: [-168.069693, 25.133463, -67.292669, 71.284212] - }); + expect(bbox).toEqual([-168.069693, 25.133463, -67.292669, 71.284212]); }); it('should return bounding box for valid country code - UAE (AE)', async () => { @@ -45,9 +38,7 @@ describe('CountryBoundingBoxTool', () => { expect(result.content[0]).toHaveProperty('type', 'text'); const textContent = result.content[0] as TextContent; const bbox = JSON.parse(textContent.text); - expect(bbox).toEqual({ - bbox: [51.590737, 22.705773, 56.376954, 26.050548] - }); + expect(bbox).toEqual([51.590737, 22.705773, 56.376954, 26.050548]); }); it('should handle lowercase country codes', async () => { @@ -57,9 +48,7 @@ describe('CountryBoundingBoxTool', () => { expect(result.content[0]).toHaveProperty('type', 'text'); const textContent = result.content[0] as TextContent; const bbox = JSON.parse(textContent.text); - expect(bbox).toEqual({ - bbox: [73.599819, 21.144707, 134.762115, 53.424591] - }); + expect(bbox).toEqual([73.599819, 21.144707, 134.762115, 53.424591]); }); it('should handle mixed case country codes', async () => { @@ -69,9 +58,7 @@ describe('CountryBoundingBoxTool', () => { expect(result.content[0]).toHaveProperty('type', 'text'); const textContent = result.content[0] as TextContent; const bbox = JSON.parse(textContent.text); - expect(bbox).toEqual({ - bbox: [-168.069693, 25.133463, -67.292669, 71.284212] - }); + expect(bbox).toEqual([-168.069693, 25.133463, -67.292669, 71.284212]); }); it('should return error for invalid country code', async () => { @@ -151,7 +138,7 @@ describe('CountryBoundingBoxTool', () => { it('should have correct input schema', async () => { const { CountryBoundingBoxSchema } = await import( - '../../../src/tools/bounding-box-tool/CountryBoundingBoxTool.input.schema.js' + '../../../src/tools/bounding-box-tool/CountryBoundingBoxTool.schema.js' ); expect(CountryBoundingBoxSchema).toBeDefined(); }); @@ -175,7 +162,7 @@ describe('CountryBoundingBoxTool', () => { if (!result.isError) { expect(result.content[0]).toHaveProperty('type', 'text'); const textContent = result.content[0] as TextContent; - const bbox = JSON.parse(textContent.text).bbox; + const bbox = JSON.parse(textContent.text); expect(Array.isArray(bbox)).toBe(true); expect(bbox).toHaveLength(4); } diff --git a/test/tools/coordinate-conversion-tool/CoordinateConversionTool.test.ts b/test/tools/coordinate-conversion-tool/CoordinateConversionTool.test.ts index a45c60d..8e64cfb 100644 --- a/test/tools/coordinate-conversion-tool/CoordinateConversionTool.test.ts +++ b/test/tools/coordinate-conversion-tool/CoordinateConversionTool.test.ts @@ -1,6 +1,3 @@ -// Copyright (c) Mapbox, Inc. -// Licensed under the MIT License. - import { describe, it, expect, beforeEach } from 'vitest'; import { CoordinateConversionTool } from '../../../src/tools/coordinate-conversion-tool/CoordinateConversionTool.js'; @@ -21,7 +18,7 @@ describe('CoordinateConversionTool', () => { it('should have correct input schema', async () => { const { CoordinateConversionSchema } = await import( - '../../../src/tools/coordinate-conversion-tool/CoordinateConversionTool.input.schema.js' + '../../../src/tools/coordinate-conversion-tool/CoordinateConversionTool.schema.js' ); expect(CoordinateConversionSchema).toBeDefined(); }); diff --git a/test/tools/create-style-tool/CreateStyleTool.test.ts b/test/tools/create-style-tool/CreateStyleTool.test.ts index 79df03d..f5beb61 100644 --- a/test/tools/create-style-tool/CreateStyleTool.test.ts +++ b/test/tools/create-style-tool/CreateStyleTool.test.ts @@ -1,11 +1,8 @@ -// Copyright (c) Mapbox, Inc. -// Licensed under the MIT License. - import { describe, it, expect, vi, afterEach, beforeAll } from 'vitest'; import { - setupHttpRequest, + setupFetch, assertHeadersSent -} from '../../utils/httpPipelineUtils.js'; +} from '../../utils/fetchRequestUtils.js'; import { CreateStyleTool } from '../../../src/tools/create-style-tool/CreateStyleTool.js'; const mockToken = @@ -22,53 +19,42 @@ describe('CreateStyleTool', () => { describe('tool metadata', () => { it('should have correct name and description', () => { - const { httpRequest } = setupHttpRequest(); - const tool = new CreateStyleTool({ httpRequest }); + const tool = new CreateStyleTool(); expect(tool.name).toBe('create_style_tool'); expect(tool.description).toBe('Create a new Mapbox style'); }); it('should have correct input schema', async () => { - const { MapboxStyleInputSchema } = await import( - '../../../src/tools/create-style-tool/CreateStyleTool.input.schema.js' + const { CreateStyleSchema } = await import( + '../../../src/tools/create-style-tool/CreateStyleTool.schema.js' ); - expect(MapboxStyleInputSchema).toBeDefined(); + expect(CreateStyleSchema).toBeDefined(); }); }); it('sends custom header', async () => { - const { httpRequest, mockHttpRequest } = setupHttpRequest({ + const { fetch, mockFetch } = setupFetch({ ok: true, - json: async () => ({ - id: 'new-style-id', - name: 'Test Style', - version: 8, - sources: {}, - layers: [] - }) + json: async () => ({ id: 'new-style-id', name: 'Test Style' }) }); - await new CreateStyleTool({ httpRequest }).run({ + await new CreateStyleTool(fetch).run({ name: 'Test Style', - version: 8, - sources: {}, - layers: [] + style: { version: 8, sources: {}, layers: [] } }); - assertHeadersSent(mockHttpRequest); + assertHeadersSent(mockFetch); }); it('handles fetch errors gracefully', async () => { - const { httpRequest, mockHttpRequest } = setupHttpRequest({ + const { fetch, mockFetch } = setupFetch({ ok: false, status: 400, statusText: 'Bad Request' }); - const result = await new CreateStyleTool({ httpRequest }).run({ + const result = await new CreateStyleTool(fetch).run({ name: 'Test Style', - version: 8, - sources: {}, - layers: [] + style: { version: 8, sources: {}, layers: [] } }); expect(result.isError).toBe(true); @@ -76,6 +62,6 @@ describe('CreateStyleTool', () => { type: 'text', text: 'Failed to create style: 400 Bad Request' }); - assertHeadersSent(mockHttpRequest); + assertHeadersSent(mockFetch); }); }); diff --git a/test/tools/create-token-tool/CreateTokenTool.test.ts b/test/tools/create-token-tool/CreateTokenTool.test.ts index bb4aa86..cb21087 100644 --- a/test/tools/create-token-tool/CreateTokenTool.test.ts +++ b/test/tools/create-token-tool/CreateTokenTool.test.ts @@ -1,14 +1,10 @@ -// Copyright (c) Mapbox, Inc. -// Licensed under the MIT License. - import { describe, it, expect, vi, afterEach, beforeAll } from 'vitest'; import { - setupHttpRequest, + setupFetch, assertHeadersSent -} from '../../utils/httpPipelineUtils.js'; +} from '../../utils/fetchRequestUtils.js'; import { MapboxApiBasedTool } from '../../../src/tools/MapboxApiBasedTool.js'; import { CreateTokenTool } from '../../../src/tools/create-token-tool/CreateTokenTool.js'; -import { HttpRequest } from 'src/utils/types.js'; // Create a token with username in the payload const payload = Buffer.from(JSON.stringify({ u: 'testuser' })).toString( @@ -27,16 +23,15 @@ describe('CreateTokenTool', () => { vi.clearAllMocks(); }); - function createTokenTool(httpRequest: HttpRequest) { - const instance = new CreateTokenTool({ httpRequest }); + function createTokenTool(fetchImpl?: typeof fetch) { + const instance = new CreateTokenTool(fetchImpl); instance['log'] = vi.fn(); return instance; } describe('tool metadata', () => { it('should have correct name and description', () => { - const { httpRequest } = setupHttpRequest(); - const tool = createTokenTool(httpRequest); + const tool = createTokenTool(); expect(tool.name).toBe('create_token_tool'); expect(tool.description).toBe( 'Create a new Mapbox public access token with specified scopes and optional URL restrictions.' @@ -45,7 +40,7 @@ describe('CreateTokenTool', () => { it('should have correct input schema', async () => { const { CreateTokenSchema } = await import( - '../../../src/tools/create-token-tool/CreateTokenTool.input.schema.js' + '../../../src/tools/create-token-tool/CreateTokenTool.schema.js' ); expect(CreateTokenSchema).toBeDefined(); }); @@ -53,8 +48,7 @@ describe('CreateTokenTool', () => { describe('validation', () => { it('validates required input fields', async () => { - const { httpRequest } = setupHttpRequest(); - const tool = createTokenTool(httpRequest); + const tool = createTokenTool(); const result = await tool.run({}); expect(result.isError).toBe(true); @@ -64,8 +58,7 @@ describe('CreateTokenTool', () => { }); it('validates allowedUrls array length', async () => { - const { httpRequest } = setupHttpRequest(); - const tool = createTokenTool(httpRequest); + const tool = createTokenTool(); const urls = new Array(101).fill('https://example.com'); @@ -82,8 +75,7 @@ describe('CreateTokenTool', () => { }); it('validates invalid scopes', async () => { - const { httpRequest } = setupHttpRequest(); - const tool = createTokenTool(httpRequest); + const tool = createTokenTool(); const result = await tool.run({ note: 'Test token', @@ -109,7 +101,8 @@ describe('CreateTokenTool', () => { vi.stubEnv('MAPBOX_ACCESS_TOKEN', invalidToken); // Setup fetch mock to prevent actual API calls - const { httpRequest } = setupHttpRequest({ + const { fetch, mockFetch } = setupFetch(); + mockFetch.mockResolvedValueOnce({ ok: true, status: 200, statusText: 'OK', @@ -117,7 +110,7 @@ describe('CreateTokenTool', () => { json: async () => ({ token: 'test-token' }) } as Response); - const toolWithInvalidToken = new CreateTokenTool({ httpRequest }); + const toolWithInvalidToken = new CreateTokenTool(fetch); toolWithInvalidToken['log'] = vi.fn(); const result = await toolWithInvalidToken.run({ @@ -149,18 +142,16 @@ describe('CreateTokenTool', () => { id: 'cktest123', scopes: ['styles:read', 'fonts:read'], created: '2024-01-01T00:00:00.000Z', - modified: '2024-01-01T00:00:00.000Z', - usage: 'pk', - client: 'api', - default: false + modified: '2024-01-01T00:00:00.000Z' }; - const { httpRequest, mockHttpRequest } = setupHttpRequest({ + const { fetch, mockFetch } = setupFetch(); + mockFetch.mockResolvedValueOnce({ ok: true, json: async () => mockResponse } as Response); - const tool = createTokenTool(httpRequest); + const tool = createTokenTool(fetch); const result = await tool.run({ note: 'Test token', @@ -179,7 +170,7 @@ describe('CreateTokenTool', () => { }); // Verify the request - expect(mockHttpRequest).toHaveBeenCalledWith( + expect(mockFetch).toHaveBeenCalledWith( `https://api.mapbox.com/tokens/v2/testuser?access_token=eyJhbGciOiJIUzI1NiJ9.${payload}.signature`, { method: 'POST', @@ -194,7 +185,7 @@ describe('CreateTokenTool', () => { ); // Verify User-Agent header was sent - assertHeadersSent(mockHttpRequest); + assertHeadersSent(mockFetch); }); it('creates a token with allowed URLs', async () => { @@ -205,18 +196,16 @@ describe('CreateTokenTool', () => { scopes: ['styles:read'], created: '2024-01-01T00:00:00.000Z', modified: '2024-01-01T00:00:00.000Z', - allowedUrls: ['https://example.com', 'https://app.example.com'], - usage: 'pk', - client: 'api', - default: false + allowedUrls: ['https://example.com', 'https://app.example.com'] }; - const { httpRequest, mockHttpRequest } = setupHttpRequest({ + const { mockFetch, fetch } = setupFetch(); + mockFetch.mockResolvedValueOnce({ ok: true, json: async () => mockResponse } as Response); - const tool = createTokenTool(httpRequest); + const tool = createTokenTool(fetch); const result = await tool.run({ note: 'Restricted token', @@ -229,7 +218,7 @@ describe('CreateTokenTool', () => { expect(responseData.allowedUrls).toEqual(mockResponse.allowedUrls); // Verify the request body included allowedUrls - const lastCall = mockHttpRequest.mock.calls[0]; + const lastCall = mockFetch.mock.calls[0]; const requestBody = JSON.parse(lastCall[1].body as string); expect(requestBody.allowedUrls).toEqual([ 'https://example.com', @@ -246,18 +235,16 @@ describe('CreateTokenTool', () => { scopes: ['styles:read'], created: '2024-01-01T00:00:00.000Z', modified: '2024-01-01T00:00:00.000Z', - expires: expiresAt, - usage: 'pk', - client: 'api', - default: false + expires: expiresAt }; - const { mockHttpRequest, httpRequest } = setupHttpRequest({ + const { mockFetch, fetch } = setupFetch(); + mockFetch.mockResolvedValueOnce({ ok: true, json: async () => mockResponse } as Response); - const tool = createTokenTool(httpRequest); + const tool = createTokenTool(fetch); const result = await tool.run({ note: 'Token with expiration', @@ -270,13 +257,14 @@ describe('CreateTokenTool', () => { expect(responseData.expires).toEqual(expiresAt); // Verify the request body included expires - const lastCall = mockHttpRequest.mock.calls[0]; + const lastCall = mockFetch.mock.calls[0]; const requestBody = JSON.parse(lastCall[1].body as string); expect(requestBody.expires).toEqual(expiresAt); }); it('handles API errors gracefully', async () => { - const { httpRequest } = setupHttpRequest({ + const { mockFetch, fetch } = setupFetch(); + mockFetch.mockResolvedValueOnce({ ok: false, status: 401, statusText: 'Unauthorized', @@ -284,7 +272,7 @@ describe('CreateTokenTool', () => { '{"message": "Token does not have required scopes", "code": "TokenScopesInvalid"}' } as Response); - const tool = createTokenTool(httpRequest); + const tool = createTokenTool(fetch); const result = await tool.run({ note: 'Test token', @@ -298,14 +286,10 @@ describe('CreateTokenTool', () => { }); it('handles network errors', async () => { - const { httpRequest } = setupHttpRequest({ - ok: false, - status: 0, - statusText: 'Network Error', - text: async () => 'Network error' - }); + const { mockFetch, fetch } = setupFetch(); + mockFetch.mockRejectedValueOnce(new Error('Network error')); - const tool = createTokenTool(httpRequest); + const tool = createTokenTool(fetch); const result = await tool.run({ note: 'Test token', @@ -315,7 +299,7 @@ describe('CreateTokenTool', () => { expect(result.isError).toBe(true); expect(result.content[0]).toHaveProperty('type', 'text'); const errorText = (result.content[0] as TextContent).text; - expect(errorText).toContain('Network Error'); + expect(errorText).toContain('Network error'); }); it('uses custom API endpoint when provided', async () => { @@ -331,18 +315,16 @@ describe('CreateTokenTool', () => { id: 'cktest', scopes: ['styles:read'], created: '2024-01-01T00:00:00.000Z', - modified: '2024-01-01T00:00:00.000Z', - usage: 'pk', - client: 'api', - default: false + modified: '2024-01-01T00:00:00.000Z' }; - const { mockHttpRequest, httpRequest } = setupHttpRequest({ + const { mockFetch, fetch } = setupFetch(); + mockFetch.mockResolvedValueOnce({ ok: true, json: async () => mockResponse } as Response); - const toolWithCustomEndpoint = new CreateTokenTool({ httpRequest }); + const toolWithCustomEndpoint = new CreateTokenTool(fetch); toolWithCustomEndpoint['log'] = vi.fn(); await toolWithCustomEndpoint.run({ @@ -350,7 +332,7 @@ describe('CreateTokenTool', () => { scopes: ['styles:read'] }); - expect(mockHttpRequest).toHaveBeenCalledWith( + expect(mockFetch).toHaveBeenCalledWith( expect.stringContaining('https://api.staging.mapbox.com/tokens/v2/'), expect.any(Object) ); diff --git a/test/tools/delete-style-tool/DeleteStyleTool.test.ts b/test/tools/delete-style-tool/DeleteStyleTool.test.ts index 6a21b08..382081d 100644 --- a/test/tools/delete-style-tool/DeleteStyleTool.test.ts +++ b/test/tools/delete-style-tool/DeleteStyleTool.test.ts @@ -1,11 +1,8 @@ -// Copyright (c) Mapbox, Inc. -// Licensed under the MIT License. - import { describe, it, expect, vi, afterEach, beforeAll } from 'vitest'; import { - setupHttpRequest, + setupFetch, assertHeadersSent -} from '../../utils/httpPipelineUtils.js'; +} from '../../utils/fetchRequestUtils.js'; import { DeleteStyleTool } from '../../../src/tools/delete-style-tool/DeleteStyleTool.js'; const mockToken = @@ -22,52 +19,80 @@ describe('DeleteStyleTool', () => { describe('tool metadata', () => { it('should have correct name and description', () => { - const { httpRequest } = setupHttpRequest(); - const tool = new DeleteStyleTool({ httpRequest }); + const tool = new DeleteStyleTool(); expect(tool.name).toBe('delete_style_tool'); expect(tool.description).toBe('Delete a Mapbox style by ID'); }); it('should have correct input schema', async () => { const { DeleteStyleSchema } = await import( - '../../../src/tools/delete-style-tool/DeleteStyleTool.input.schema.js' + '../../../src/tools/delete-style-tool/DeleteStyleTool.schema.js' ); expect(DeleteStyleSchema).toBeDefined(); }); }); it('returns success for 204 No Content', async () => { - const { httpRequest, mockHttpRequest } = setupHttpRequest({ + const { fetch, mockFetch } = setupFetch({ ok: true, status: 204 }); - const result = await new DeleteStyleTool({ httpRequest }).run({ + const result = await new DeleteStyleTool(fetch).run({ styleId: 'style-123' }); expect(result.content[0]).toEqual({ type: 'text', - text: 'Style deleted successfully' + text: '{"success":true,"message":"Style deleted successfully"}' + }); + assertHeadersSent(mockFetch); + }); + + it('returns response body for non-204 success', async () => { + const { fetch, mockFetch } = setupFetch({ + ok: true, + status: 200, + json: async () => ({ deleted: true }) + }); + + const result = await new DeleteStyleTool(fetch).run({ + styleId: 'style-123' + }); + + expect(result.isError).toBe(false); + expect(result.content[0]).toMatchObject({ + type: 'text', + text: `{"deleted":true}` }); - assertHeadersSent(mockHttpRequest); + assertHeadersSent(mockFetch); }); it('handles fetch errors gracefully', async () => { - const { httpRequest, mockHttpRequest } = setupHttpRequest({ + const { fetch, mockFetch } = setupFetch({ ok: false, status: 404, statusText: 'Not Found' }); - const result = await new DeleteStyleTool({ httpRequest }).run({ - styleId: 'style-123' - }); + let result; + try { + result = await new DeleteStyleTool(fetch).run({ + styleId: 'style-123' + }); + } catch (e) { + if (e instanceof Error) { + expect(e.message).toContain('Failed to update style: 404 Not Found'); + } else { + expect.fail('Thrown error is not an instance of Error'); + } + return; + } expect(result.isError).toBe(true); expect(result.content[0]).toMatchObject({ type: 'text', text: 'Failed to delete style: 404 Not Found' }); - assertHeadersSent(mockHttpRequest); + assertHeadersSent(mockFetch); }); }); diff --git a/test/tools/geojson-preview-tool/GeojsonPreviewTool.test.ts b/test/tools/geojson-preview-tool/GeojsonPreviewTool.test.ts index f62b670..e28159b 100644 --- a/test/tools/geojson-preview-tool/GeojsonPreviewTool.test.ts +++ b/test/tools/geojson-preview-tool/GeojsonPreviewTool.test.ts @@ -1,6 +1,3 @@ -// Copyright (c) Mapbox, Inc. -// Licensed under the MIT License. - import { describe, it, expect, vi, afterEach } from 'vitest'; import { GeojsonPreviewTool } from '../../../src/tools/geojson-preview-tool/GeojsonPreviewTool.js'; @@ -20,7 +17,7 @@ describe('GeojsonPreviewTool', () => { it('should have correct input schema', async () => { const { GeojsonPreviewSchema } = await import( - '../../../src/tools/geojson-preview-tool/GeojsonPreviewTool.input.schema.js' + '../../../src/tools/geojson-preview-tool/GeojsonPreviewTool.schema.js' ); expect(GeojsonPreviewSchema).toBeDefined(); }); diff --git a/test/tools/get-mapbox-doc-source-tool/GetMapboxDocSourceTool.test.ts b/test/tools/get-mapbox-doc-source-tool/GetMapboxDocSourceTool.test.ts index 6180576..7e93f52 100644 --- a/test/tools/get-mapbox-doc-source-tool/GetMapboxDocSourceTool.test.ts +++ b/test/tools/get-mapbox-doc-source-tool/GetMapboxDocSourceTool.test.ts @@ -1,14 +1,10 @@ -// Copyright (c) Mapbox, Inc. -// Licensed under the MIT License. - import { describe, expect, it } from 'vitest'; import { GetMapboxDocSourceTool } from '../../../src/tools/get-mapbox-doc-source-tool/GetMapboxDocSourceTool.js'; -import { setupHttpRequest } from '../../utils/httpPipelineUtils.js'; +import { setupFetch } from 'test/utils/fetchRequestUtils.js'; describe('GetMapboxDocSourceTool', () => { it('should have correct name and description', () => { - const { httpRequest } = setupHttpRequest(); - const tool = new GetMapboxDocSourceTool({ httpRequest }); + const tool = new GetMapboxDocSourceTool(); expect(tool.name).toBe('get_latest_mapbox_docs_tool'); expect(tool.description).toContain( @@ -30,23 +26,21 @@ This is the Mapbox developer documentation for LLMs. ## APIs - Geocoding API for address search - Directions API for routing`; - const { httpRequest, mockHttpRequest } = setupHttpRequest({ + + const { fetch, mockFetch } = setupFetch({ ok: true, status: 200, text: () => Promise.resolve(mockContent) }); - const tool = new GetMapboxDocSourceTool({ httpRequest }); + const tool = new GetMapboxDocSourceTool(fetch); const result = await tool.run({}); - expect(mockHttpRequest).toHaveBeenCalledWith( - 'https://docs.mapbox.com/llms.txt', - { - headers: { - 'User-Agent': 'TestServer/1.0.0 (default, no-tag, abcdef)' - } + expect(mockFetch).toHaveBeenCalledWith('https://docs.mapbox.com/llms.txt', { + headers: { + 'User-Agent': 'TestServer/1.0.0 (default, no-tag, abcdef)' } - ); + }); expect(result.content).toHaveLength(1); expect(result.content[0].type).toBe('text'); @@ -57,13 +51,12 @@ This is the Mapbox developer documentation for LLMs. }); it('should handle HTTP errors', async () => { - const { httpRequest } = setupHttpRequest({ + const { fetch } = setupFetch({ ok: false, - status: 404, - statusText: 'Not Found' + status: 404 }); - const tool = new GetMapboxDocSourceTool({ httpRequest }); + const tool = new GetMapboxDocSourceTool(fetch); const result = await tool.run({}); @@ -74,16 +67,17 @@ This is the Mapbox developer documentation for LLMs. expect(result.content[0].text).toContain( 'Failed to fetch Mapbox documentation' ); - expect(result.content[0].text).toContain('Not Found'); + expect(result.content[0].text).toContain('HTTP error! status: 404'); } }); it('should handle network errors', async () => { - const { httpRequest } = setupHttpRequest({ + const { fetch } = setupFetch({ text: () => Promise.reject(new Error('Network error')) }); - const tool = new GetMapboxDocSourceTool({ httpRequest }); + const tool = new GetMapboxDocSourceTool(fetch); + const result = await tool.run({}); expect(result.isError).toBe(true); @@ -98,11 +92,12 @@ This is the Mapbox developer documentation for LLMs. }); it('should handle unknown errors', async () => { - const { httpRequest } = setupHttpRequest({ + const { fetch } = setupFetch({ text: () => Promise.reject(new Error('Unknown error occurred')) }); - const tool = new GetMapboxDocSourceTool({ httpRequest }); + const tool = new GetMapboxDocSourceTool(fetch); + const result = await tool.run({}); expect(result.isError).toBe(true); @@ -119,13 +114,13 @@ This is the Mapbox developer documentation for LLMs. it('should work with empty input object', async () => { const mockContent = 'Test documentation content'; - const { httpRequest } = setupHttpRequest({ + const { fetch } = setupFetch({ ok: true, status: 200, text: () => Promise.resolve(mockContent) }); - const tool = new GetMapboxDocSourceTool({ httpRequest }); + const tool = new GetMapboxDocSourceTool(fetch); const result = await tool.run({}); diff --git a/test/tools/list-styles-tool/ListStylesTool.test.ts b/test/tools/list-styles-tool/ListStylesTool.test.ts index 0f85d1f..2db891e 100644 --- a/test/tools/list-styles-tool/ListStylesTool.test.ts +++ b/test/tools/list-styles-tool/ListStylesTool.test.ts @@ -1,11 +1,8 @@ -// Copyright (c) Mapbox, Inc. -// Licensed under the MIT License. - import { describe, it, expect, vi, afterEach, beforeAll } from 'vitest'; import { - setupHttpRequest, + setupFetch, assertHeadersSent -} from '../../utils/httpPipelineUtils.js'; +} from '../../utils/fetchRequestUtils.js'; import { ListStylesTool } from '../../../src/tools/list-styles-tool/ListStylesTool.js'; const mockToken = 'sk.eyJ1IjoidGVzdC11c2VyIiwiYSI6InRlc3QtYXBpIn0.signature'; @@ -21,8 +18,7 @@ describe('ListStylesTool', () => { describe('tool metadata', () => { it('should have correct name and description', () => { - const { httpRequest } = setupHttpRequest(); - const tool = new ListStylesTool({ httpRequest }); + const tool = new ListStylesTool(); expect(tool.name).toBe('list_styles_tool'); expect(tool.description).toBe( 'List styles for a Mapbox account. Use limit parameter to avoid large responses (recommended: limit=5-10). Use start parameter for pagination.' @@ -31,14 +27,14 @@ describe('ListStylesTool', () => { it('should have correct input schema', async () => { const { ListStylesSchema } = await import( - '../../../src/tools/list-styles-tool/ListStylesTool.input.schema.js' + '../../../src/tools/list-styles-tool/ListStylesTool.schema.js' ); expect(ListStylesSchema).toBeDefined(); }); }); it('sends custom header', async () => { - const { httpRequest, mockHttpRequest } = setupHttpRequest({ + const { fetch, mockFetch } = setupFetch({ ok: true, json: async () => [ { id: 'style1', name: 'Test Style 1' }, @@ -46,203 +42,100 @@ describe('ListStylesTool', () => { ] }); - await new ListStylesTool({ httpRequest }).run({}); - assertHeadersSent(mockHttpRequest); + await new ListStylesTool(fetch).run({}); + assertHeadersSent(mockFetch); }); it('handles fetch errors gracefully', async () => { - const { httpRequest, mockHttpRequest } = setupHttpRequest({ + const { fetch, mockFetch } = setupFetch({ ok: false, status: 404, statusText: 'Not Found' }); - const result = await new ListStylesTool({ httpRequest }).run({}); + const result = await new ListStylesTool(fetch).run({}); expect(result.isError).toBe(true); expect(result.content[0]).toMatchObject({ type: 'text', text: 'Failed to list styles: 404 Not Found' }); - assertHeadersSent(mockHttpRequest); - }); - - it('handles scope/permission errors with helpful message', async () => { - const mockHeaders = new Map([['content-type', 'application/json']]); - const { httpRequest, mockHttpRequest } = setupHttpRequest({ - ok: false, - status: 403, - statusText: 'Forbidden', - headers: { - get: (name: string) => mockHeaders.get(name.toLowerCase()) - } as Headers, - json: async () => ({ - message: 'This API requires a token with styles:list scope.' - }) - }); - - const result = await new ListStylesTool({ httpRequest }).run({}); - - expect(result.isError).toBe(true); - expect(result.content[0].type).toBe('text'); - const errorText = (result.content[0] as { type: 'text'; text: string }) - .text; - expect(errorText).toContain( - 'This API requires a token with styles:list scope' - ); - expect(errorText).toContain('appropriate scopes'); - expect(errorText).toContain('MAPBOX_ACCESS_TOKEN'); - assertHeadersSent(mockHttpRequest); + assertHeadersSent(mockFetch); }); it('extracts username from token for API call', async () => { - const { httpRequest, mockHttpRequest } = setupHttpRequest({ + const { fetch, mockFetch } = setupFetch({ ok: true, json: async () => [] }); - await new ListStylesTool({ httpRequest }).run({}); + await new ListStylesTool(fetch).run({}); - expect(mockHttpRequest).toHaveBeenCalledWith( + expect(mockFetch).toHaveBeenCalledWith( expect.stringContaining('/styles/v1/test-user?access_token='), expect.any(Object) ); - assertHeadersSent(mockHttpRequest); + assertHeadersSent(mockFetch); }); it('includes limit parameter when provided', async () => { - const { httpRequest, mockHttpRequest } = setupHttpRequest({ + const { fetch, mockFetch } = setupFetch({ ok: true, json: async () => [] }); - await new ListStylesTool({ httpRequest }).run({ limit: 10 }); + await new ListStylesTool(fetch).run({ limit: 10 }); - expect(mockHttpRequest).toHaveBeenCalledWith( + expect(mockFetch).toHaveBeenCalledWith( expect.stringMatching(/\/styles\/v1\/test-user\?.*limit=10/), expect.any(Object) ); - assertHeadersSent(mockHttpRequest); + assertHeadersSent(mockFetch); }); it('includes start parameter when provided', async () => { - const { httpRequest, mockHttpRequest } = setupHttpRequest({ + const { fetch, mockFetch } = setupFetch({ ok: true, json: async () => [] }); - await new ListStylesTool({ httpRequest }).run({ start: 'abc123' }); + await new ListStylesTool(fetch).run({ start: 'abc123' }); - expect(mockHttpRequest).toHaveBeenCalledWith( + expect(mockFetch).toHaveBeenCalledWith( expect.stringMatching(/\/styles\/v1\/test-user\?.*start=abc123/), expect.any(Object) ); - assertHeadersSent(mockHttpRequest); + assertHeadersSent(mockFetch); }); it('includes both limit and start parameters when provided', async () => { - const { httpRequest, mockHttpRequest } = setupHttpRequest({ + const { fetch, mockFetch } = setupFetch({ ok: true, json: async () => [] }); - await new ListStylesTool({ httpRequest }).run({ - limit: 5, - start: 'xyz789' - }); + await new ListStylesTool(fetch).run({ limit: 5, start: 'xyz789' }); - const calledUrl = mockHttpRequest.mock.calls[0][0]; + const calledUrl = mockFetch.mock.calls[0][0]; expect(calledUrl).toMatch(/\/styles\/v1\/test-user\?/); expect(calledUrl).toMatch(/limit=5/); expect(calledUrl).toMatch(/start=xyz789/); expect(calledUrl).toMatch(/access_token=/); - assertHeadersSent(mockHttpRequest); + assertHeadersSent(mockFetch); }); it('returns style list on success', async () => { const mockStyles = [ - { - id: 'style1', - name: 'Test Style 1', - owner: 'testuser', - created: '2020-05-05T08:27:39.280Z', - modified: '2020-05-05T08:27:41.353Z', - visibility: 'private' as const, - version: 8, - sources: {}, - layers: [] - }, - { - id: 'style2', - name: 'Test Style 2', - owner: 'testuser', - created: '2020-05-06T08:27:39.280Z', - modified: '2020-05-06T08:27:41.353Z', - visibility: 'public' as const, - version: 8, - sources: {}, - layers: [] - } - ]; - - const { httpRequest, mockHttpRequest } = setupHttpRequest({ - ok: true, - json: async () => mockStyles - }); - - const result = await new ListStylesTool({ httpRequest }).run({}); - - expect(result.isError).toBe(false); - expect(result.content).toHaveLength(1); - expect(result.content[0].type).toBe('text'); - - const content = result.content[0]; - if (content.type === 'text') { - const parsedResponse = JSON.parse(content.text); - expect(parsedResponse).toHaveProperty('styles'); - expect(parsedResponse.styles).toEqual(mockStyles); - } - - // Verify structuredContent has the expected shape - if (result.structuredContent) { - expect(result.structuredContent).toHaveProperty('styles'); - expect( - (result.structuredContent as { styles: unknown[] }).styles - ).toEqual(mockStyles); - } - - assertHeadersSent(mockHttpRequest); - }); - - it('handles styles without layers field (real API response)', async () => { - // This matches the actual production API response format - const mockStyles = [ - { - center: [139.7667, 35.681249], - created: '2020-05-05T08:27:39.280Z', - id: 'ck9tnguii0ipm1ipf54wqhhwm', - modified: '2020-05-05T08:27:41.353Z', - name: 'Yahoo! Japan Streets', - owner: 'svc-okta-mapbox-staff-access', - sources: { - composite: { - url: 'mapbox://mapbox.mapbox-streets-v8,mapbox.road-detail-v1-33,mapbox.transit-v2', - type: 'vector' - } - }, - version: 8, - visibility: 'private' as const, - zoom: 16, - protected: false - } + { id: 'style1', name: 'Test Style 1', owner: 'testuser' }, + { id: 'style2', name: 'Test Style 2', owner: 'testuser' } ]; - const { httpRequest, mockHttpRequest } = setupHttpRequest({ + const { fetch, mockFetch } = setupFetch({ ok: true, json: async () => mockStyles }); - const result = await new ListStylesTool({ httpRequest }).run({}); + const result = await new ListStylesTool(fetch).run({}); expect(result.isError).toBe(false); expect(result.content).toHaveLength(1); @@ -251,20 +144,9 @@ describe('ListStylesTool', () => { const content = result.content[0]; if (content.type === 'text') { const parsedResponse = JSON.parse(content.text); - expect(parsedResponse).toHaveProperty('styles'); - expect(parsedResponse.styles).toHaveLength(1); - expect(parsedResponse.styles[0]).toMatchObject({ - id: 'ck9tnguii0ipm1ipf54wqhhwm', - name: 'Yahoo! Japan Streets', - owner: 'svc-okta-mapbox-staff-access', - visibility: 'private', - version: 8, - protected: false - }); - // Verify layers field is not required - expect(parsedResponse.styles[0].layers).toBeUndefined(); + expect(parsedResponse).toEqual(mockStyles); } - assertHeadersSent(mockHttpRequest); + assertHeadersSent(mockFetch); }); }); diff --git a/test/tools/list-tokens-tool/ListTokensTool.test.ts b/test/tools/list-tokens-tool/ListTokensTool.test.ts index d4fc7a8..d5418d8 100644 --- a/test/tools/list-tokens-tool/ListTokensTool.test.ts +++ b/test/tools/list-tokens-tool/ListTokensTool.test.ts @@ -1,21 +1,20 @@ -// Copyright (c) Mapbox, Inc. -// Licensed under the MIT License. - -import { describe, it, expect, afterEach, vi } from 'vitest'; +import { describe, it, expect, afterEach, vi, beforeAll } from 'vitest'; import { - setupHttpRequest, + setupFetch, assertHeadersSent -} from '../../utils/httpPipelineUtils.js'; +} from '../../utils/fetchRequestUtils.js'; import { MapboxApiBasedTool } from '../../../src/tools/MapboxApiBasedTool.js'; import { ListTokensTool } from '../../../src/tools/list-tokens-tool/ListTokensTool.js'; -import { HttpRequest } from '../../../src//utils/types.js'; // Create a token with username in the payload const payload = Buffer.from(JSON.stringify({ u: 'testuser' })).toString( 'base64' ); const mockToken = `eyJhbGciOiJIUzI1NiJ9.${payload}.signature`; -process.env.MAPBOX_ACCESS_TOKEN = mockToken; + +beforeAll(() => { + process.env.MAPBOX_ACCESS_TOKEN = mockToken; +}); type TextContent = { type: 'text'; text: string }; @@ -24,8 +23,8 @@ describe('ListTokensTool', () => { vi.clearAllMocks(); }); - function createListTokensTool(httpRequest: HttpRequest): ListTokensTool { - const tool = new ListTokensTool({ httpRequest }); + function createListTokensTool(fetchImpl?: typeof fetch): ListTokensTool { + const tool = new ListTokensTool(fetchImpl); // Mock the log method to prevent actual logging during tests tool['log'] = vi.fn(); return tool; @@ -33,8 +32,7 @@ describe('ListTokensTool', () => { describe('tool metadata', () => { it('should have correct name and description', () => { - const { httpRequest } = setupHttpRequest(); - const tool = createListTokensTool(httpRequest); + const tool = createListTokensTool(); expect(tool.name).toBe('list_tokens_tool'); expect(tool.description).toBe( @@ -44,7 +42,7 @@ describe('ListTokensTool', () => { it('should have correct input schema', async () => { const { ListTokensSchema } = await import( - '../../../src/tools/list-tokens-tool/ListTokensTool.input.schema.js' + '../../../src/tools/list-tokens-tool/ListTokensTool.schema.js' ); expect(ListTokensSchema).toBeDefined(); }); @@ -52,8 +50,7 @@ describe('ListTokensTool', () => { describe('validation', () => { it('validates limit range', async () => { - const { httpRequest } = setupHttpRequest(); - const tool = createListTokensTool(httpRequest); + const tool = createListTokensTool(); const result = await tool.run({ limit: 101 }); expect(result.isError).toBe(true); @@ -63,8 +60,7 @@ describe('ListTokensTool', () => { }); it('validates sortby enum values', async () => { - const { httpRequest } = setupHttpRequest(); - const tool = createListTokensTool(httpRequest); + const tool = createListTokensTool(); const result = await tool.run({ sortby: 'invalid' as unknown as 'created' | 'modified' }); @@ -74,8 +70,7 @@ describe('ListTokensTool', () => { }); it('validates usage enum values', async () => { - const { httpRequest } = setupHttpRequest(); - const tool = createListTokensTool(httpRequest); + const tool = createListTokensTool(); const result = await tool.run({ usage: 'invalid' as unknown as 'pk' }); @@ -97,7 +92,8 @@ describe('ListTokensTool', () => { vi.stubEnv('MAPBOX_ACCESS_TOKEN', invalidToken); // Setup fetch mock to prevent actual API calls - const { httpRequest } = setupHttpRequest({ + const { mockFetch, fetch } = setupFetch(); + mockFetch.mockResolvedValueOnce({ ok: true, status: 200, statusText: 'OK', @@ -105,7 +101,7 @@ describe('ListTokensTool', () => { json: async () => [] } as Response); - const toolWithInvalidToken = createListTokensTool(httpRequest); + const toolWithInvalidToken = createListTokensTool(fetch); const result = await toolWithInvalidToken.run({}); @@ -132,33 +128,30 @@ describe('ListTokensTool', () => { id: 'cktest123', note: 'Default public token', usage: 'pk', - client: 'api', token: 'pk.eyJ1IjoidGVzdHVzZXIifQ.test123', scopes: ['styles:read', 'fonts:read'], created: '2023-01-01T00:00:00.000Z', - modified: '2023-01-01T00:00:00.000Z', - default: false + modified: '2023-01-01T00:00:00.000Z' }, { id: 'cktest456', note: 'Secret token', usage: 'sk', - client: 'api', token: 'sk.eyJ1IjoidGVzdHVzZXIifQ.test456', scopes: ['styles:read', 'fonts:read', 'tokens:read'], created: '2023-02-01T00:00:00.000Z', - modified: '2023-02-01T00:00:00.000Z', - default: false + modified: '2023-02-01T00:00:00.000Z' } ]; - const { httpRequest, mockHttpRequest } = setupHttpRequest({ + const { mockFetch, fetch } = setupFetch(); + mockFetch.mockResolvedValueOnce({ ok: true, headers: new Headers(), json: async () => mockTokens } as Response); - const tool = createListTokensTool(httpRequest); + const tool = createListTokensTool(fetch); const result = await tool.run({}); @@ -172,7 +165,7 @@ describe('ListTokensTool', () => { expect(responseData.tokens[1].id).toBe('cktest456'); // Verify the request - expect(mockHttpRequest).toHaveBeenCalledWith( + expect(mockFetch).toHaveBeenCalledWith( expect.stringContaining( 'https://api.mapbox.com/tokens/v2/testuser?access_token=' ), @@ -185,7 +178,7 @@ describe('ListTokensTool', () => { ); // Verify User-Agent header was sent - assertHeadersSent(mockHttpRequest); + assertHeadersSent(mockFetch); }); it('filters by default token', async () => { @@ -194,7 +187,6 @@ describe('ListTokensTool', () => { id: 'ckdefault', note: 'Default public token', usage: 'pk', - client: 'api', token: 'pk.eyJ1IjoidGVzdHVzZXIifQ.default', default: true, scopes: ['styles:read', 'fonts:read'], @@ -203,14 +195,14 @@ describe('ListTokensTool', () => { } ]; - const { httpRequest, mockHttpRequest } = setupHttpRequest(); - mockHttpRequest.mockResolvedValueOnce({ + const { mockFetch, fetch } = setupFetch(); + mockFetch.mockResolvedValueOnce({ ok: true, headers: new Headers(), json: async () => mockTokens } as Response); - const tool = createListTokensTool(httpRequest); + const tool = createListTokensTool(fetch); const result = await tool.run({ default: true }); expect(result.isError).toBe(false); @@ -219,7 +211,7 @@ describe('ListTokensTool', () => { expect(responseData.tokens[0].default).toBe(true); // Verify the request included the default parameter - expect(mockHttpRequest).toHaveBeenCalledWith( + expect(mockFetch).toHaveBeenCalledWith( expect.stringContaining('default=true'), expect.any(Object) ); @@ -231,29 +223,27 @@ describe('ListTokensTool', () => { id: 'cktest789', note: 'Token 3', usage: 'pk', - client: 'api', token: 'pk.eyJ1IjoidGVzdHVzZXIifQ.test789', scopes: ['styles:read'], created: '2023-03-01T00:00:00.000Z', - modified: '2023-03-01T00:00:00.000Z', - default: false + modified: '2023-03-01T00:00:00.000Z' } ]; - const { httpRequest, mockHttpRequest } = setupHttpRequest(); + const { mockFetch, fetch } = setupFetch(); const headers = new Headers(); headers.set( 'Link', '; rel="next"' ); - mockHttpRequest.mockResolvedValueOnce({ + mockFetch.mockResolvedValueOnce({ ok: true, headers, json: async () => mockTokens } as Response); - const tool = createListTokensTool(httpRequest); + const tool = createListTokensTool(fetch); const result = await tool.run({ limit: 10, start: 'cktest789', @@ -265,7 +255,7 @@ describe('ListTokensTool', () => { expect(responseData.tokens).toHaveLength(1); // Verify all parameters were included in the request - const callUrl = mockHttpRequest.mock.calls[0][0] as string; + const callUrl = mockFetch.mock.calls[0][0] as string; expect(callUrl).toContain('limit=10'); expect(callUrl).toContain('start=cktest789'); expect(callUrl).toContain('sortby=created'); @@ -277,29 +267,27 @@ describe('ListTokensTool', () => { id: 'cktest789', note: 'Token 3', usage: 'pk', - client: 'api', token: 'pk.eyJ1IjoidGVzdHVzZXIifQ.test789', scopes: ['styles:read'], created: '2023-03-01T00:00:00.000Z', - modified: '2023-03-01T00:00:00.000Z', - default: false + modified: '2023-03-01T00:00:00.000Z' } ]; - const { httpRequest, mockHttpRequest } = setupHttpRequest(); + const { mockFetch, fetch } = setupFetch(); const headers = new Headers(); headers.set( 'Link', '; rel="next"' ); - mockHttpRequest.mockResolvedValueOnce({ + mockFetch.mockResolvedValueOnce({ ok: true, headers, json: async () => mockTokens } as Response); - const tool = createListTokensTool(httpRequest); + const tool = createListTokensTool(fetch); const result = await tool.run({ limit: 10 }); @@ -315,28 +303,27 @@ describe('ListTokensTool', () => { id: 'cktest789', note: 'Token 3', usage: 'pk', - client: 'api', token: 'pk.eyJ1IjoidGVzdHVzZXIifQ.test789', scopes: ['styles:read'], created: '2023-03-01T00:00:00.000Z', - modified: '2023-03-01T00:00:00.000Z', - default: false + modified: '2023-03-01T00:00:00.000Z' } ]; + const { mockFetch, fetch } = setupFetch(); const headers = new Headers(); headers.set( 'Link', '; rel="next"' ); - const { httpRequest } = setupHttpRequest({ + mockFetch.mockResolvedValueOnce({ ok: true, headers, json: async () => mockTokens - }); + } as Response); - const tool = createListTokensTool(httpRequest); + const tool = createListTokensTool(fetch); const result = await tool.run({ start: 'cktest789' }); @@ -352,15 +339,14 @@ describe('ListTokensTool', () => { id: 'cktest789', note: 'Token 3', usage: 'pk', - client: 'api', token: 'pk.eyJ1IjoidGVzdHVzZXIifQ.test789', scopes: ['styles:read'], created: '2023-03-01T00:00:00.000Z', - modified: '2023-03-01T00:00:00.000Z', - default: false + modified: '2023-03-01T00:00:00.000Z' } ]; + const { mockFetch, fetch } = setupFetch(); // First page with Link header const headers1 = new Headers(); headers1.set( @@ -371,22 +357,19 @@ describe('ListTokensTool', () => { // Second page without Link header (end of results) const headers2 = new Headers(); - const { httpRequest, mockHttpRequest } = setupHttpRequest(); - // Reset from default response - mockHttpRequest.mockReset(); - mockHttpRequest.mockResolvedValueOnce({ + mockFetch.mockResolvedValueOnce({ ok: true, headers: headers1, json: async () => mockTokens } as Response); - mockHttpRequest.mockResolvedValueOnce({ + mockFetch.mockResolvedValueOnce({ ok: true, headers: headers2, json: async () => [] // Empty array for second page } as Response); - const tool = createListTokensTool(httpRequest); + const tool = createListTokensTool(fetch); const result = await tool.run({}); @@ -402,23 +385,21 @@ describe('ListTokensTool', () => { id: 'cktest789', note: 'Token 3', usage: 'pk', - client: 'api', token: 'pk.eyJ1IjoidGVzdHVzZXIifQ.test789', scopes: ['styles:read'], created: '2023-03-01T00:00:00.000Z', - modified: '2023-03-01T00:00:00.000Z', - default: false + modified: '2023-03-01T00:00:00.000Z' } ]; - const { httpRequest, mockHttpRequest } = setupHttpRequest(); - mockHttpRequest.mockResolvedValueOnce({ + const { mockFetch, fetch } = setupFetch(); + mockFetch.mockResolvedValueOnce({ ok: true, headers: new Headers(), json: async () => mockTokens } as Response); - const tool = createListTokensTool(httpRequest); + const tool = createListTokensTool(fetch); const result = await tool.run({ limit: 10 }); @@ -434,23 +415,21 @@ describe('ListTokensTool', () => { id: 'pktest123', note: 'Public token', usage: 'pk', - client: 'api', token: 'pk.eyJ1IjoidGVzdHVzZXIifQ.pub123', scopes: ['styles:read'], created: '2023-04-01T00:00:00.000Z', - modified: '2023-04-01T00:00:00.000Z', - default: false + modified: '2023-04-01T00:00:00.000Z' } ]; - const { httpRequest, mockHttpRequest } = setupHttpRequest(); - mockHttpRequest.mockResolvedValueOnce({ + const { mockFetch, fetch } = setupFetch(); + mockFetch.mockResolvedValueOnce({ ok: true, headers: new Headers(), json: async () => mockTokens } as Response); - const tool = createListTokensTool(httpRequest); + const tool = createListTokensTool(fetch); const result = await tool.run({ usage: 'pk' }); @@ -460,15 +439,15 @@ describe('ListTokensTool', () => { expect(responseData.tokens[0].usage).toBe('pk'); // Verify the usage parameter was included - expect(mockHttpRequest).toHaveBeenCalledWith( + expect(mockFetch).toHaveBeenCalledWith( expect.stringContaining('usage=pk'), expect.any(Object) ); }); it('handles API errors gracefully', async () => { - const { httpRequest, mockHttpRequest } = setupHttpRequest(); - mockHttpRequest.mockResolvedValueOnce({ + const { mockFetch, fetch } = setupFetch(); + mockFetch.mockResolvedValueOnce({ ok: false, status: 401, statusText: 'Unauthorized', @@ -476,7 +455,7 @@ describe('ListTokensTool', () => { '{"message": "Invalid access token", "code": "TokenInvalid"}' } as Response); - const tool = createListTokensTool(httpRequest); + const tool = createListTokensTool(fetch); const result = await tool.run({}); @@ -487,10 +466,10 @@ describe('ListTokensTool', () => { }); it('handles network errors', async () => { - const { httpRequest, mockHttpRequest } = setupHttpRequest(); - mockHttpRequest.mockRejectedValueOnce(new Error('Network error')); + const { mockFetch, fetch } = setupFetch(); + mockFetch.mockRejectedValueOnce(new Error('Network error')); - const tool = createListTokensTool(httpRequest); + const tool = createListTokensTool(fetch); const result = await tool.run({}); expect(result.isError).toBe(true); @@ -508,18 +487,18 @@ describe('ListTokensTool', () => { const mockTokens: object[] = []; - const { httpRequest, mockHttpRequest } = setupHttpRequest(); - mockHttpRequest.mockResolvedValueOnce({ + const { mockFetch, fetch } = setupFetch(); + mockFetch.mockResolvedValueOnce({ ok: true, headers: new Headers(), json: async () => mockTokens } as Response); - const tool = createListTokensTool(httpRequest); + const tool = createListTokensTool(fetch); await tool.run({}); - expect(mockHttpRequest).toHaveBeenCalledWith( + expect(mockFetch).toHaveBeenCalledWith( expect.stringContaining('https://api.staging.mapbox.com/tokens/v2/'), expect.any(Object) ); @@ -539,24 +518,20 @@ describe('ListTokensTool', () => { id: 'cktest123', note: 'Test token', usage: 'pk', - client: 'api', token: 'pk.test', - scopes: ['styles:read'], - created: '2023-04-01T00:00:00.000Z', - modified: '2023-04-01T00:00:00.000Z', - default: false + scopes: ['styles:read'] } ] }; - const { httpRequest, mockHttpRequest } = setupHttpRequest(); - mockHttpRequest.mockResolvedValueOnce({ + const { mockFetch, fetch } = setupFetch(); + mockFetch.mockResolvedValueOnce({ ok: true, headers: new Headers(), json: async () => mockResponse } as Response); - const tool = createListTokensTool(httpRequest); + const tool = createListTokensTool(fetch); const result = await tool.run({}); expect(result.isError).toBe(false); diff --git a/test/tools/preview-style-tool/PreviewStyleTool.test.ts b/test/tools/preview-style-tool/PreviewStyleTool.test.ts index ea70336..d98b82c 100644 --- a/test/tools/preview-style-tool/PreviewStyleTool.test.ts +++ b/test/tools/preview-style-tool/PreviewStyleTool.test.ts @@ -1,6 +1,3 @@ -// Copyright (c) Mapbox, Inc. -// Licensed under the MIT License. - process.env.MAPBOX_ACCESS_TOKEN = 'sk.eyJhbGciOiJIUzI1NiJ9.eyJ1IjoidGVzdC11c2VyIiwiYSI6InRlc3QtYXBpIn0.signature'; @@ -22,7 +19,7 @@ describe('PreviewStyleTool', () => { it('should have correct input schema', async () => { const { PreviewStyleSchema } = await import( - '../../../src/tools/preview-style-tool/PreviewStyleTool.input.schema.js' + '../../../src/tools/preview-style-tool/PreviewStyleTool.schema.js' ); expect(PreviewStyleSchema).toBeDefined(); }); @@ -31,9 +28,7 @@ describe('PreviewStyleTool', () => { it('uses user-provided public token and returns preview URL', async () => { const result = await new PreviewStyleTool().run({ styleId: 'test-style', - accessToken: TEST_ACCESS_TOKEN, - title: false, - zoomwheel: false + accessToken: TEST_ACCESS_TOKEN }); expect(result.isError).toBe(false); @@ -48,9 +43,7 @@ describe('PreviewStyleTool', () => { it('includes styleId in URL', async () => { const result = await new PreviewStyleTool().run({ styleId: 'my-custom-style', - accessToken: TEST_ACCESS_TOKEN, - title: false, - zoomwheel: false + accessToken: TEST_ACCESS_TOKEN }); expect(result.content[0]).toMatchObject({ @@ -63,8 +56,7 @@ describe('PreviewStyleTool', () => { const result = await new PreviewStyleTool().run({ styleId: 'test-style', accessToken: TEST_ACCESS_TOKEN, - title: true, - zoomwheel: false + title: true }); expect(result.content[0]).toMatchObject({ @@ -77,8 +69,7 @@ describe('PreviewStyleTool', () => { const result = await new PreviewStyleTool().run({ styleId: 'test-style', accessToken: TEST_ACCESS_TOKEN, - zoomwheel: false, - title: false + zoomwheel: false }); expect(result.content[0]).toMatchObject({ @@ -90,9 +81,7 @@ describe('PreviewStyleTool', () => { it('includes fresh parameter for secure access', async () => { const result = await new PreviewStyleTool().run({ styleId: 'test-style', - accessToken: TEST_ACCESS_TOKEN, - title: false, - zoomwheel: false + accessToken: TEST_ACCESS_TOKEN }); expect(result.content[0]).toMatchObject({ @@ -105,9 +94,7 @@ describe('PreviewStyleTool', () => { const result = await new PreviewStyleTool().run({ styleId: 'test-style', accessToken: - 'sk.eyJhbGciOiJIUzI1NiJ9.eyJ1IjoidGVzdC11c2VyIn0.secret_token', - title: false, - zoomwheel: false + 'sk.eyJhbGciOiJIUzI1NiJ9.eyJ1IjoidGVzdC11c2VyIn0.secret_token' }); expect(result.isError).toBe(true); @@ -122,9 +109,7 @@ describe('PreviewStyleTool', () => { it('rejects temporary tokens', async () => { const result = await new PreviewStyleTool().run({ styleId: 'test-style', - accessToken: 'tk.eyJhbGciOiJIUzI1NiJ9.eyJ1IjoidGVzdC11c2VyIn0.temp_token', - title: false, - zoomwheel: false + accessToken: 'tk.eyJhbGciOiJIUzI1NiJ9.eyJ1IjoidGVzdC11c2VyIn0.temp_token' }); expect(result.isError).toBe(true); @@ -139,9 +124,7 @@ describe('PreviewStyleTool', () => { it('returns URL on success', async () => { const result = await new PreviewStyleTool().run({ styleId: 'test-style', - accessToken: TEST_ACCESS_TOKEN, - title: false, - zoomwheel: false + accessToken: TEST_ACCESS_TOKEN }); expect(result.isError).toBe(false); diff --git a/test/tools/retrieve-style-tool/RetrieveStyleTool.test.ts b/test/tools/retrieve-style-tool/RetrieveStyleTool.test.ts index 4d0858b..1f7115f 100644 --- a/test/tools/retrieve-style-tool/RetrieveStyleTool.test.ts +++ b/test/tools/retrieve-style-tool/RetrieveStyleTool.test.ts @@ -1,11 +1,8 @@ -// Copyright (c) Mapbox, Inc. -// Licensed under the MIT License. - import { describe, it, expect, vi, afterEach, beforeAll } from 'vitest'; import { - setupHttpRequest, + setupFetch, assertHeadersSent -} from '../../utils/httpPipelineUtils.js'; +} from '../../utils/fetchRequestUtils.js'; import { RetrieveStyleTool } from '../../../src/tools/retrieve-style-tool/RetrieveStyleTool.js'; const mockToken = @@ -22,15 +19,14 @@ describe('RetrieveStyleTool', () => { describe('tool metadata', () => { it('should have correct name and description', () => { - const { httpRequest } = setupHttpRequest(); - const tool = new RetrieveStyleTool({ httpRequest }); + const tool = new RetrieveStyleTool(); expect(tool.name).toBe('retrieve_style_tool'); expect(tool.description).toBe('Retrieve a specific Mapbox style by ID'); }); it('should have correct input schema', async () => { const { RetrieveStyleSchema } = await import( - '../../../src/tools/retrieve-style-tool/RetrieveStyleTool.input.schema.js' + '../../../src/tools/retrieve-style-tool/RetrieveStyleTool.schema.js' ); expect(RetrieveStyleSchema).toBeDefined(); }); @@ -38,25 +34,25 @@ describe('RetrieveStyleTool', () => { it('returns style data for successful fetch', async () => { const styleData = { id: 'style-123', name: 'Test Style' }; - const { httpRequest, mockHttpRequest } = setupHttpRequest({ + const { fetch, mockFetch } = setupFetch({ ok: true, status: 200, json: async () => styleData }); - const result = await new RetrieveStyleTool({ httpRequest }).run({ + const result = await new RetrieveStyleTool(fetch).run({ styleId: 'style-123' }); expect(result.content[0]).toMatchObject({ type: 'text', - text: JSON.stringify(styleData, null, 2) + text: JSON.stringify(styleData) }); - assertHeadersSent(mockHttpRequest); + assertHeadersSent(mockFetch); }); it('handles fetch errors gracefully', async () => { - const { httpRequest, mockHttpRequest } = setupHttpRequest({ + const { fetch, mockFetch } = setupFetch({ ok: false, status: 404, statusText: 'Not Found' @@ -64,7 +60,7 @@ describe('RetrieveStyleTool', () => { let result; try { - result = await new RetrieveStyleTool({ httpRequest }).run({ + result = await new RetrieveStyleTool(fetch).run({ styleId: 'style-456' }); } catch (e) { @@ -81,59 +77,6 @@ describe('RetrieveStyleTool', () => { type: 'text', text: 'Failed to retrieve style: 404 Not Found' }); - assertHeadersSent(mockHttpRequest); - }); - - it('handles styles with null terrain and other nullable fields', async () => { - // Real-world API response with null values for optional fields - const styleData = { - id: 'cjxyz123', - name: 'Production Style', - owner: 'test-user', - version: 8, - created: '2020-01-01T00:00:00.000Z', - modified: '2020-01-02T00:00:00.000Z', - visibility: 'private' as const, - sources: { - composite: { - type: 'vector' as const, - url: 'mapbox://mapbox.mapbox-streets-v8' - } - }, - layers: [ - { - id: 'background', - type: 'background' as const, - paint: { 'background-color': '#000000' } - } - ], - terrain: null, // API returns null instead of omitting the field - fog: null, - lights: null - }; - - const { httpRequest, mockHttpRequest } = setupHttpRequest({ - ok: true, - status: 200, - json: async () => styleData - }); - - const result = await new RetrieveStyleTool({ httpRequest }).run({ - styleId: 'cjxyz123' - }); - - expect(result.isError).toBe(false); - expect(result.content[0].type).toBe('text'); - - const content = result.content[0]; - if (content.type === 'text') { - const parsedResponse = JSON.parse(content.text); - expect(parsedResponse.terrain).toBeNull(); - expect(parsedResponse.fog).toBeNull(); - expect(parsedResponse.lights).toBeNull(); - expect(parsedResponse.id).toBe('cjxyz123'); - } - - assertHeadersSent(mockHttpRequest); + assertHeadersSent(mockFetch); }); }); diff --git a/test/tools/style-builder-tool/StyleBuilderTool.test.ts b/test/tools/style-builder-tool/StyleBuilderTool.test.ts index 97ce8ca..b405f62 100644 --- a/test/tools/style-builder-tool/StyleBuilderTool.test.ts +++ b/test/tools/style-builder-tool/StyleBuilderTool.test.ts @@ -1,9 +1,6 @@ -// Copyright (c) Mapbox, Inc. -// Licensed under the MIT License. - import { describe, it, expect, beforeEach } from 'vitest'; import { StyleBuilderTool } from '../../../src/tools/style-builder-tool/StyleBuilderTool.js'; -import type { StyleBuilderToolInput } from '../../../src/tools/style-builder-tool/StyleBuilderTool.input.schema.js'; +import type { StyleBuilderToolInput } from '../../../src/tools/style-builder-tool/StyleBuilderTool.schema.js'; describe('StyleBuilderTool', () => { let tool: StyleBuilderTool; @@ -26,18 +23,17 @@ describe('StyleBuilderTool', () => { { layer_type: 'water', action: 'color', - color: '#0066ff', - render_type: 'symbol' + color: '#0066ff' } ] }; - const result = await tool.run(input); + const result = await tool.execute(input); expect(result.isError).toBe(false); expect(result.content[0].type).toBe('text'); - const text = result.content[0].text as string; + const text = result.content[0].text; expect(text).toContain('Style Built Successfully'); expect(text).toContain('Test Style'); expect(text).toContain('"#0066ff"'); @@ -46,7 +42,7 @@ describe('StyleBuilderTool', () => { it('should handle dark mode', async () => { const input: StyleBuilderToolInput = { style_name: 'Dark Mode Style', - base_style: 'streets-v12', // Use classic style to test background color + base_style: 'streets', // Use classic style to test background color layers: [], global_settings: { mode: 'dark', @@ -54,10 +50,10 @@ describe('StyleBuilderTool', () => { } }; - const result = await tool.run(input); + const result = await tool.execute(input); expect(result.isError).toBe(false); - const text = result.content[0].text as string; + const text = result.content[0].text; expect(text).toContain('Mode:** dark'); expect(text).toContain('#000000'); }); @@ -73,14 +69,13 @@ describe('StyleBuilderTool', () => { layer_type: 'road', action: 'color', color: '#ff0000', - filter_properties: { class: 'primary' }, - render_type: 'symbol' + filter_properties: { class: 'primary' } } ] }; - const result = await tool.run(input); - const text = result.content[0].text as string; + const result = await tool.execute(input); + const text = result.content[0].text; expect(result.isError).toBe(false); expect(text).toContain('#ff0000'); @@ -96,14 +91,13 @@ describe('StyleBuilderTool', () => { action: 'highlight', color: '#ffff00', width: 5, - filter_properties: { class: 'major_rail' }, - render_type: 'symbol' + filter_properties: { class: 'major_rail' } } ] }; - const result = await tool.run(input); - const text = result.content[0].text as string; + const result = await tool.execute(input); + const text = result.content[0].text; expect(result.isError).toBe(false); expect(text).toContain('Highlighted'); @@ -117,14 +111,13 @@ describe('StyleBuilderTool', () => { layers: [ { layer_type: 'place_label', - action: 'hide', - render_type: 'symbol' + action: 'hide' } ] }; - const result = await tool.run(input); - const text = result.content[0].text as string; + const result = await tool.execute(input); + const text = result.content[0].text; expect(result.isError).toBe(false); expect(text).toContain('Hidden'); @@ -137,14 +130,13 @@ describe('StyleBuilderTool', () => { layers: [ { layer_type: 'building', - action: 'show', - render_type: 'symbol' + action: 'show' } ] }; - const result = await tool.run(input); - const text = result.content[0].text as string; + const result = await tool.execute(input); + const text = result.content[0].text; expect(result.isError).toBe(false); expect(text).toContain('Shown'); @@ -162,14 +154,13 @@ describe('StyleBuilderTool', () => { action: 'color', color: '#ff0000', width: 3, - filter_properties: { admin_level: 0, maritime: 'false' }, - render_type: 'symbol' + filter_properties: { admin_level: 0, maritime: 'false' } } ] }; - const result = await tool.run(input); - const text = result.content[0].text as string; + const result = await tool.execute(input); + const text = result.content[0].text; expect(result.isError).toBe(false); @@ -204,14 +195,13 @@ describe('StyleBuilderTool', () => { action: 'color', color: '#0000ff', opacity: 0.5, - filter_properties: { admin_level: 1, maritime: 'false' }, - render_type: 'symbol' + filter_properties: { admin_level: 1, maritime: 'false' } } ] }; - const result = await tool.run(input); - const text = result.content[0].text as string; + const result = await tool.execute(input); + const text = result.content[0].text; expect(result.isError).toBe(false); @@ -240,21 +230,19 @@ describe('StyleBuilderTool', () => { { layer_type: 'water', action: 'color', - color: '#0099ff', - render_type: 'symbol' + color: '#0099ff' }, { layer_type: 'landuse', filter_properties: { class: 'park' }, action: 'color', - color: '#00ff00', - render_type: 'symbol' + color: '#00ff00' } ] }; - const result = await tool.run(input); - const text = result.content[0].text as string; + const result = await tool.execute(input); + const text = result.content[0].text; const jsonMatch = text.match(/```json\n([\s\S]*?)\n```/); expect(jsonMatch).toBeTruthy(); @@ -285,12 +273,12 @@ describe('StyleBuilderTool', () => { // Test with classic style const input: StyleBuilderToolInput = { style_name: 'Essential Layers Test', - base_style: 'streets-v12', // Use classic style + base_style: 'streets', // Use classic style layers: [] // No layers specified }; - const result = await tool.run(input); - const text = result.content[0].text as string; + const result = await tool.execute(input); + const text = result.content[0].text; const jsonMatch = text.match(/```json\n([\s\S]*?)\n```/); const style = JSON.parse(jsonMatch![1]); @@ -312,17 +300,16 @@ describe('StyleBuilderTool', () => { { layer_type: 'unknown_layer' as any, action: 'color', - color: '#ff0000', - render_type: 'symbol' + color: '#ff0000' } ] }; - const result = await tool.run(input); + const result = await tool.execute(input); // Should return help message, not error expect(result.isError).toBe(false); - const text = result.content[0].text as string; + const text = result.content[0].text; expect(text).toContain('not found'); expect(text).toContain('Available source layers'); }); @@ -336,16 +323,15 @@ describe('StyleBuilderTool', () => { layer_type: 'road', action: 'color', color: '#ff0000', - filter: ['==', ['get', 'class'], 'motorway'], - render_type: 'symbol' + filter: ['==', ['get', 'class'], 'motorway'] } ] }; - const result = await tool.run(input); + const result = await tool.execute(input); expect(result.isError).toBe(false); - const text = result.content[0].text as string; + const text = result.content[0].text; const jsonMatch = text.match(/```json\n([\s\S]*?)\n```/); const style = JSON.parse(jsonMatch![1]); @@ -377,10 +363,10 @@ describe('StyleBuilderTool', () => { ] }; - const result = await tool.run(input); + const result = await tool.execute(input); expect(result.isError).toBe(false); - const text = result.content[0].text as string; + const text = result.content[0].text; const jsonMatch = text.match(/```json\n([\s\S]*?)\n```/); const style = JSON.parse(jsonMatch![1]); @@ -416,10 +402,10 @@ describe('StyleBuilderTool', () => { ] }; - const result = await tool.run(input); + const result = await tool.execute(input); expect(result.isError).toBe(false); - const text = result.content[0].text as string; + const text = result.content[0].text; const jsonMatch = text.match(/```json\n([\s\S]*?)\n```/); const style = JSON.parse(jsonMatch![1]); @@ -456,10 +442,10 @@ describe('StyleBuilderTool', () => { ] }; - const result = await tool.run(input); + const result = await tool.execute(input); expect(result.isError).toBe(false); - const text = result.content[0].text as string; + const text = result.content[0].text; const jsonMatch = text.match(/```json\n([\s\S]*?)\n```/); const style = JSON.parse(jsonMatch![1]); @@ -490,10 +476,10 @@ describe('StyleBuilderTool', () => { ] }; - const result = await tool.run(input); + const result = await tool.execute(input); expect(result.isError).toBe(false); - const text = result.content[0].text as string; + const text = result.content[0].text; const jsonMatch = text.match(/```json\n([\s\S]*?)\n```/); const style = JSON.parse(jsonMatch![1]); @@ -517,7 +503,7 @@ describe('StyleBuilderTool', () => { const tool = new StyleBuilderTool(); const input: StyleBuilderToolInput = { style_name: 'Transit Test', - base_style: 'streets-v12', + base_style: 'streets', layers: [ { layer_type: 'transit', @@ -525,17 +511,17 @@ describe('StyleBuilderTool', () => { color: '#ff0000', filter_properties: { maki: 'bus' - }, - render_type: 'symbol' + } } ] }; - const result = await tool.run(input); + const result = await tool.execute(input); expect(result.isError).toBe(false); - const text = result.content[0].text as string; - const styleJson = JSON.parse(text.match(/```json\n([\s\S]*?)\n```/)![1]); + const styleJson = JSON.parse( + result.content[0].text.match(/```json\n([\s\S]*?)\n```/)![1] + ); const transitLayer = styleJson.layers.find((l: any) => l.id.includes('transit') @@ -554,24 +540,24 @@ describe('StyleBuilderTool', () => { const tool = new StyleBuilderTool(); const input: StyleBuilderToolInput = { style_name: 'Multi Transit Test', - base_style: 'streets-v12', + base_style: 'streets', layers: [ { layer_type: 'transit', action: 'highlight', filter_properties: { maki: ['bus', 'entrance', 'rail-metro'] - }, - render_type: 'symbol' + } } ] }; - const result = await tool.run(input); + const result = await tool.execute(input); expect(result.isError).toBe(false); - const text = result.content[0].text as string; - const styleJson = JSON.parse(text.match(/```json\n([\s\S]*?)\n```/)![1]); + const styleJson = JSON.parse( + result.content[0].text.match(/```json\n([\s\S]*?)\n```/)![1] + ); const transitLayer = styleJson.layers.find((l: any) => l.id.includes('transit') @@ -605,11 +591,12 @@ describe('StyleBuilderTool', () => { ] }; - const result = await tool.run(input); + const result = await tool.execute(input); expect(result.isError).toBe(false); - const text = result.content[0].text as string; - const styleJson = JSON.parse(text.match(/```json\n([\s\S]*?)\n```/)![1]); + const styleJson = JSON.parse( + result.content[0].text.match(/```json\n([\s\S]*?)\n```/)![1] + ); const roadsLayer = styleJson.layers.find((l: any) => l.id.includes('road-toll-true') @@ -631,16 +618,15 @@ describe('StyleBuilderTool', () => { color: '#ff0000', filter_properties: { class: 'motorway' - }, - render_type: 'symbol' + } } ] }; - const result = await tool.run(input); + const result = await tool.execute(input); expect(result.isError).toBe(false); - const text = result.content[0].text as string; + const text = result.content[0].text; const jsonMatch = text.match(/```json\n([\s\S]*?)\n```/); const style = JSON.parse(jsonMatch![1]); @@ -664,16 +650,15 @@ describe('StyleBuilderTool', () => { filter_properties: { class: ['motorway', 'trunk'], structure: 'bridge' - }, - render_type: 'symbol' + } } ] }; - const result = await tool.run(input); + const result = await tool.execute(input); expect(result.isError).toBe(false); - const text = result.content[0].text as string; + const text = result.content[0].text; const jsonMatch = text.match(/```json\n([\s\S]*?)\n```/); const style = JSON.parse(jsonMatch![1]); @@ -700,16 +685,15 @@ describe('StyleBuilderTool', () => { admin_level: 0, disputed: 'false', maritime: 'false' - }, - render_type: 'symbol' + } } ] }; - const result = await tool.run(input); + const result = await tool.execute(input); expect(result.isError).toBe(false); - const text = result.content[0].text as string; + const text = result.content[0].text; const jsonMatch = text.match(/```json\n([\s\S]*?)\n```/); const style = JSON.parse(jsonMatch![1]); @@ -733,14 +717,13 @@ describe('StyleBuilderTool', () => { { layer_type: 'water', action: 'color', - color: '#0099ff', - render_type: 'symbol' + color: '#0099ff' } ] }; - const result = await tool.run(input); - const text = result.content[0].text as string; + const result = await tool.execute(input); + const text = result.content[0].text; const jsonMatch = text.match(/```json\n([\s\S]*?)\n```/); const style = JSON.parse(jsonMatch![1]); @@ -772,8 +755,7 @@ describe('StyleBuilderTool', () => { { layer_type: 'water', action: 'color', - color: '#0099ff', - render_type: 'symbol' + color: '#0099ff' } ], standard_config: { @@ -802,8 +784,8 @@ describe('StyleBuilderTool', () => { } }; - const result = await tool.run(input); - const text = result.content[0].text as string; + const result = await tool.execute(input); + const text = result.content[0].text; expect(text).toContain('Standard Config:** 15 properties set'); expect(text).toContain('Theme: faded'); @@ -834,7 +816,7 @@ describe('StyleBuilderTool', () => { it('should generate Classic style with sources', async () => { const input: StyleBuilderToolInput = { style_name: 'Classic Style Test', - base_style: 'streets-v12', + base_style: 'streets', layers: [ { layer_type: 'water', @@ -844,8 +826,8 @@ describe('StyleBuilderTool', () => { ] }; - const result = await tool.run(input); - const text = result.content[0].text as string; + const result = await tool.execute(input); + const text = result.content[0].text; const jsonMatch = text.match(/```json\n([\s\S]*?)\n```/); const style = JSON.parse(jsonMatch![1]); @@ -876,28 +858,25 @@ describe('StyleBuilderTool', () => { layer_type: 'water', action: 'color', color: '#0099ff', - slot: 'bottom', - render_type: 'symbol' + slot: 'bottom' }, { layer_type: 'landuse', filter_properties: { class: 'park' }, action: 'color', color: '#00ff00', - slot: 'middle', - render_type: 'symbol' + slot: 'middle' }, { layer_type: 'poi_label', action: 'show', - slot: 'top', - render_type: 'symbol' + slot: 'top' } ] }; - const result = await tool.run(input); - const text = result.content[0].text as string; + const result = await tool.execute(input); + const text = result.content[0].text; const jsonMatch = text.match(/```json\n([\s\S]*?)\n```/); const style = JSON.parse(jsonMatch![1]); @@ -933,12 +912,11 @@ describe('StyleBuilderTool', () => { action: 'color', color: '#0099ff' } - ], - base_style: 'standard' + ] }; - const result = await tool.run(input); - const text = result.content[0].text as string; + const result = await tool.execute(input); + const text = result.content[0].text; const jsonMatch = text.match(/```json\n([\s\S]*?)\n```/); const style = JSON.parse(jsonMatch![1]); @@ -970,16 +948,15 @@ describe('StyleBuilderTool', () => { color: '#00ff00', filter_properties: { type: ['wetland', 'swamp'] - }, - render_type: 'symbol' + } } ] }; - const result = await tool.run(input); + const result = await tool.execute(input); expect(result.isError).toBe(false); - const text = result.content[0].text as string; + const text = result.content[0].text; // No longer expecting auto-correction since we're using the correct layer expect(text).toContain('Style Built Successfully'); @@ -1013,16 +990,15 @@ describe('StyleBuilderTool', () => { color: '#ff0000', filter_properties: { maki: 'restaurant' // This field only exists in poi_label - }, - render_type: 'symbol' + } } ] }; - const result = await tool.run(input); + const result = await tool.execute(input); expect(result.isError).toBe(false); - const text = result.content[0].text as string; + const text = result.content[0].text; expect(text).toContain( 'Determined source layer "poi_label" from filter properties' ); @@ -1038,33 +1014,29 @@ describe('StyleBuilderTool', () => { { layer_type: 'water', action: 'color', - color: '#0066ff', - render_type: 'symbol' + color: '#0066ff' }, { layer_type: 'landuse', filter_properties: { class: 'park' }, action: 'highlight', - color: '#00ff00', - render_type: 'symbol' + color: '#00ff00' }, { layer_type: 'place_label', - action: 'hide', - render_type: 'symbol' + action: 'hide' }, { layer_type: 'building', - action: 'show', - render_type: 'symbol' + action: 'show' } ] }; - const result = await tool.run(input); + const result = await tool.execute(input); expect(result.isError).toBe(false); - const text = result.content[0].text as string; + const text = result.content[0].text; expect(text).toContain('Layers Configured:** 4'); expect(text).toContain('Set to #0066ff'); diff --git a/test/tools/style-comparison-tool/StyleComparisonTool.test.ts b/test/tools/style-comparison-tool/StyleComparisonTool.test.ts index 4856ebf..df8189f 100644 --- a/test/tools/style-comparison-tool/StyleComparisonTool.test.ts +++ b/test/tools/style-comparison-tool/StyleComparisonTool.test.ts @@ -1,9 +1,6 @@ -// Copyright (c) Mapbox, Inc. -// Licensed under the MIT License. - import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { MapboxApiBasedTool } from '../../../src/tools/MapboxApiBasedTool.js'; import { StyleComparisonTool } from '../../../src/tools/style-comparison-tool/StyleComparisonTool.js'; -import * as jwtUtils from '../../../src/utils/jwtUtils.js'; describe('StyleComparisonTool', () => { let tool: StyleComparisonTool; @@ -13,7 +10,7 @@ describe('StyleComparisonTool', () => { }); afterEach(() => { - vi.restoreAllMocks(); + vi.clearAllMocks(); }); describe('run', () => { @@ -40,7 +37,7 @@ describe('StyleComparisonTool', () => { before: 'mapbox/streets-v12', after: 'mapbox/satellite-v9' // Missing accessToken - } as any; + }; const result = await tool.run(input); @@ -66,7 +63,10 @@ describe('StyleComparisonTool', () => { }); it('should handle just style IDs with valid public token', async () => { - vi.spyOn(jwtUtils, 'getUserNameFromToken').mockReturnValue('testuser'); + // Mock MapboxApiBasedTool.getUserNameFromToken to return a username + vi.spyOn(MapboxApiBasedTool, 'getUserNameFromToken').mockReturnValue( + 'testuser' + ); const input = { before: 'style-id-1', @@ -102,8 +102,8 @@ describe('StyleComparisonTool', () => { it('should reject invalid token formats', async () => { const input = { - before: 'streets-v12', - after: 'outdoors-v12', + before: 'mapbox/streets-v12', + after: 'mapbox/outdoors-v12', accessToken: 'invalid.token' }; @@ -117,11 +117,13 @@ describe('StyleComparisonTool', () => { it('should return error for style ID without valid username in token', async () => { // Mock getUserNameFromToken to throw an error - vi.spyOn(jwtUtils, 'getUserNameFromToken').mockImplementation(() => { - throw new Error( - 'MAPBOX_ACCESS_TOKEN does not contain username in payload' - ); - }); + vi.spyOn(MapboxApiBasedTool, 'getUserNameFromToken').mockImplementation( + () => { + throw new Error( + 'MAPBOX_ACCESS_TOKEN does not contain username in payload' + ); + } + ); const input = { before: 'style-id-only', @@ -134,7 +136,7 @@ describe('StyleComparisonTool', () => { expect(result.isError).toBe(true); expect( (result.content[0] as { type: 'text'; text: string }).text - ).toContain('Could not determine username for style ID'); + ).toContain('Could not determine username'); }); it('should properly encode URL parameters', async () => { diff --git a/test/tools/tilequery-tool/TilequeryTool.test.ts b/test/tools/tilequery-tool/TilequeryTool.test.ts index 8acd906..444d436 100644 --- a/test/tools/tilequery-tool/TilequeryTool.test.ts +++ b/test/tools/tilequery-tool/TilequeryTool.test.ts @@ -1,17 +1,12 @@ -// Copyright (c) Mapbox, Inc. -// Licensed under the MIT License. - import { describe, it, expect, beforeEach } from 'vitest'; import { TilequeryTool } from '../../../src/tools/tilequery-tool/TilequeryTool.js'; -import { TilequeryInput } from '../../../src/tools/tilequery-tool/TilequeryTool.input.schema.js'; -import { setupHttpRequest } from '../../utils/httpPipelineUtils.js'; +import { TilequeryInput } from '../../../src/tools/tilequery-tool/TilequeryTool.schema.js'; describe('TilequeryTool', () => { let tool: TilequeryTool; beforeEach(() => { - const { httpRequest } = setupHttpRequest(); - tool = new TilequeryTool({ httpRequest }); + tool = new TilequeryTool(); }); describe('constructor', () => { diff --git a/test/tools/tool-naming-convention.test.ts b/test/tools/tool-naming-convention.test.ts index 1de0c80..14d8a28 100644 --- a/test/tools/tool-naming-convention.test.ts +++ b/test/tools/tool-naming-convention.test.ts @@ -3,7 +3,6 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; import { BaseTool } from '../../src/tools/BaseTool.js'; import { pathToFileURL } from 'node:url'; -import { setupHttpRequest } from '../utils/httpPipelineUtils.js'; async function discoverTools(): Promise { const toolsDir = path.resolve( @@ -12,9 +11,6 @@ async function discoverTools(): Promise { ); const tools: any[] = []; - // Setup httpRequest for tools that need it - const { httpRequest } = setupHttpRequest(); - // Find all directories that end with '-tool' const entries = fs.readdirSync(toolsDir, { withFileTypes: true }); const toolDirectories = entries @@ -43,17 +39,7 @@ async function discoverTools(): Promise { ); for (const toolClass of toolClasses) { - try { - // Try to instantiate with httpRequest parameter (for MapboxApiBasedTool subclasses) - tools.push(new (toolClass as any)({ httpRequest })); - } catch (error) { - // Fall back to no-arg constructor (for other tools) - try { - tools.push(new (toolClass as any)()); - } catch (innerError) { - throw error; // Re-throw the original error - } - } + tools.push(new (toolClass as any)()); } } catch (error) { console.warn( diff --git a/test/tools/update-style-tool/UpdateStyleTool.test.ts b/test/tools/update-style-tool/UpdateStyleTool.test.ts index 987815e..518ca12 100644 --- a/test/tools/update-style-tool/UpdateStyleTool.test.ts +++ b/test/tools/update-style-tool/UpdateStyleTool.test.ts @@ -1,11 +1,8 @@ -// Copyright (c) Mapbox, Inc. -// Licensed under the MIT License. - import { describe, it, expect, vi, afterEach, beforeAll } from 'vitest'; import { - setupHttpRequest, + setupFetch, assertHeadersSent -} from '../../utils/httpPipelineUtils.js'; +} from '../../utils/fetchRequestUtils.js'; import { UpdateStyleTool } from '../../../src/tools/update-style-tool/UpdateStyleTool.js'; const mockToken = @@ -22,36 +19,35 @@ describe('UpdateStyleTool', () => { describe('tool metadata', () => { it('should have correct name and description', () => { - const { httpRequest } = setupHttpRequest(); - const tool = new UpdateStyleTool({ httpRequest }); + const tool = new UpdateStyleTool(); expect(tool.name).toBe('update_style_tool'); expect(tool.description).toBe('Update an existing Mapbox style'); }); it('should have correct input schema', async () => { - const { MapboxStyleInputSchema } = await import( - '../../../src/tools/update-style-tool/UpdateStyleTool.input.schema.js' + const { UpdateStyleSchema } = await import( + '../../../src/tools/update-style-tool/UpdateStyleTool.schema.js' ); - expect(MapboxStyleInputSchema).toBeDefined(); + expect(UpdateStyleSchema).toBeDefined(); }); }); it('sends custom header', async () => { - const { httpRequest, mockHttpRequest } = setupHttpRequest({ + const { fetch, mockFetch } = setupFetch({ ok: true, json: async () => ({ id: 'updated-style-id', name: 'Updated Style' }) }); - await new UpdateStyleTool({ httpRequest }).run({ + await new UpdateStyleTool(fetch).run({ styleId: 'style-123', name: 'Updated Style', style: { version: 8, sources: {}, layers: [] } }); - assertHeadersSent(mockHttpRequest); + assertHeadersSent(mockFetch); }); it('handles fetch errors gracefully', async () => { - const { httpRequest, mockHttpRequest } = setupHttpRequest({ + const { fetch, mockFetch } = setupFetch({ ok: false, status: 404, statusText: 'Not Found' @@ -59,7 +55,7 @@ describe('UpdateStyleTool', () => { let result; try { - result = await new UpdateStyleTool({ httpRequest }).run({ + result = await new UpdateStyleTool(fetch).run({ styleId: 'style-123', name: 'Updated Style', style: { version: 8, sources: {}, layers: [] } @@ -78,6 +74,6 @@ describe('UpdateStyleTool', () => { type: 'text', text: 'Failed to update style: 404 Not Found' }); - assertHeadersSent(mockHttpRequest); + assertHeadersSent(mockFetch); }); }); diff --git a/test/utils/fetchRequest.test.ts b/test/utils/fetchRequest.test.ts new file mode 100644 index 0000000..754c629 --- /dev/null +++ b/test/utils/fetchRequest.test.ts @@ -0,0 +1,325 @@ +import { describe, it, expect, vi, afterEach } from 'vitest'; +import { + RetryPolicy, + PolicyPipeline, + UserAgentPolicy +} from '../../src/utils/fetchRequest.js'; +import type { Mock } from 'vitest'; + +function createMockFetch( + responses: Array<{ status: number; ok?: boolean }> +): typeof fetch { + let call = 0; + return vi.fn(async (_input: string | URL | Request, _init?: RequestInit) => { + const res = responses[Math.min(call, responses.length - 1)]; + call++; + return { + ok: res.ok ?? res.status < 400, + status: res.status, + statusText: `Status ${res.status}`, + json: async () => ({ status: res.status }) + } as Response; + }) as typeof fetch; +} + +describe('PolicyPipeline', () => { + describe('usePolicy, removePolicy, and listPolicies', () => { + it('adds policies with usePolicy', () => { + const pipeline = new PolicyPipeline(); + const policy1 = new UserAgentPolicy('Agent1'); + const policy2 = new RetryPolicy(); + + pipeline.usePolicy(policy1); + pipeline.usePolicy(policy2); + + const policies = pipeline.listPolicies(); + expect(policies).toHaveLength(2); + expect(policies[0]).toBe(policy1); + expect(policies[1]).toBe(policy2); + }); + + it('removes policies with removePolicy', () => { + const pipeline = new PolicyPipeline(); + const policy1 = new UserAgentPolicy('Agent1'); + const policy2 = new RetryPolicy(); + const policy3 = new UserAgentPolicy('Agent3'); + + pipeline.usePolicy(policy1); + pipeline.usePolicy(policy2); + pipeline.usePolicy(policy3); + + pipeline.removePolicy(policy2); + + const policies = pipeline.listPolicies(); + expect(policies).toHaveLength(2); + expect(policies[0]).toBe(policy1); + expect(policies[1]).toBe(policy3); + }); + + it('removePolicy does nothing if policy not found', () => { + const pipeline = new PolicyPipeline(); + const policy1 = new UserAgentPolicy('Agent1'); + const policy2 = new RetryPolicy(); + + pipeline.usePolicy(policy1); + + pipeline.removePolicy(policy2); // Not in the list + + const policies = pipeline.listPolicies(); + expect(policies).toHaveLength(1); + expect(policies[0]).toBe(policy1); + }); + + it('listPolicies returns empty array initially', () => { + const pipeline = new PolicyPipeline(); + expect(pipeline.listPolicies()).toEqual([]); + }); + + it('listPolicies returns the policies array', () => { + const pipeline = new PolicyPipeline(); + const policy = new UserAgentPolicy('Agent1'); + + pipeline.usePolicy(policy); + const policies1 = pipeline.listPolicies(); + const policies2 = pipeline.listPolicies(); + + expect(policies1).toBe(policies2); // Same reference + expect(policies1).toEqual(policies2); // Same content + expect(policies1).toContain(policy); + }); + }); + + describe('RetryPolicy', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('retries on 500 and returns last response after max retries', async () => { + const mockFetch = createMockFetch([ + { status: 500 }, + { status: 500 }, + { status: 500 }, + { status: 500 } + ]); + const pipeline = new PolicyPipeline(mockFetch); + pipeline.usePolicy(new RetryPolicy(3, 1, 10)); // Use small delays for test speed + + const response = await pipeline.fetch('http://test', {}); + + expect(mockFetch).toHaveBeenCalledTimes(4); + expect(response.status).toBe(500); + }); + + it('retries on 429 and succeeds if later response is ok', async () => { + const mockFetch = createMockFetch([ + { status: 429 }, + { status: 429 }, + { status: 200, ok: true } + ]); + const pipeline = new PolicyPipeline(mockFetch); + pipeline.usePolicy(new RetryPolicy(3, 1, 10)); + + const response = await pipeline.fetch('http://test', {}); + + expect(mockFetch).toHaveBeenCalledTimes(3); + expect(response.status).toBe(200); + expect(response.ok).toBe(true); + }); + + it('does not retry on 400 errors', async () => { + const mockFetch = createMockFetch([{ status: 400 }]); + const pipeline = new PolicyPipeline(mockFetch); + pipeline.usePolicy(new RetryPolicy(3, 1, 10)); + + const response = await pipeline.fetch('http://test', {}); + + expect(mockFetch).toHaveBeenCalledTimes(1); + expect(response.status).toBe(400); + }); + + it('returns immediately on first success', async () => { + const mockFetch = createMockFetch([{ status: 200, ok: true }]); + const pipeline = new PolicyPipeline(mockFetch); + pipeline.usePolicy(new RetryPolicy(3, 1, 10)); + + const response = await pipeline.fetch('http://test', {}); + + expect(mockFetch).toHaveBeenCalledTimes(1); + expect(response.status).toBe(200); + expect(response.ok).toBe(true); + }); + }); + + describe('UserAgentPolicy', () => { + it('sets the User-Agent header if not present', async () => { + const mockFetch = vi.fn( + async (input: string | URL | Request, init?: RequestInit) => { + return { + ok: true, + status: 200, + statusText: 'OK', + json: async () => ({}), + headers: init?.headers + } as Response; + } + ) as Mock; + + const pipeline = new PolicyPipeline(mockFetch as unknown as typeof fetch); + pipeline.usePolicy(new UserAgentPolicy('TestAgent/1.0')); + + await pipeline.fetch('http://test', {}); + + const headers = mockFetch.mock.calls[0][1]?.headers as Record< + string, + string + >; + expect(headers['User-Agent']).toBe('TestAgent/1.0'); + }); + + it('does not overwrite an existing User-Agent header', async () => { + const mockFetch = vi.fn( + async (input: string | URL | Request, init?: RequestInit) => { + return { + ok: true, + status: 200, + statusText: 'OK', + json: async () => ({}), + headers: init?.headers + } as Response; + } + ) as Mock; + + const pipeline = new PolicyPipeline(mockFetch as unknown as typeof fetch); + pipeline.usePolicy(new UserAgentPolicy('TestAgent/1.0')); + + await pipeline.fetch('http://test', { + headers: { + 'User-Agent': 'CustomAgent/2.0' + } + }); + + const headers = mockFetch.mock.calls[0][1]?.headers as Record< + string, + string + >; + expect(headers['User-Agent']).toBe('CustomAgent/2.0'); + }); + + it('works with headers as Headers object', async () => { + const mockFetch = vi.fn( + async (_input: string | URL | Request, init?: RequestInit) => { + return { + ok: true, + status: 200, + statusText: 'OK', + json: async () => ({}), + headers: init?.headers + } as Response; + } + ) as Mock; + + const pipeline = new PolicyPipeline(mockFetch as unknown as typeof fetch); + pipeline.usePolicy(new UserAgentPolicy('TestAgent/1.0')); + + const headers = new Headers(); + await pipeline.fetch('http://test', { headers }); + + expect(headers.get('User-Agent')).toBe('TestAgent/1.0'); + }); + }); + + describe('Policy ID functionality', () => { + it('assigns unique IDs to policies when not provided', () => { + const policy1 = new UserAgentPolicy('Agent1'); + const policy2 = new UserAgentPolicy('Agent2'); + const policy3 = new RetryPolicy(); + + expect(policy1.id).toBeDefined(); + expect(policy2.id).toBeDefined(); + expect(policy3.id).toBeDefined(); + expect(policy1.id).not.toBe(policy2.id); + expect(policy2.id).not.toBe(policy3.id); + }); + + it('uses custom ID when provided', () => { + const customId = 'my-custom-policy'; + const policy = new UserAgentPolicy('Agent1', customId); + + expect(policy.id).toBe(customId); + }); + + it('removes policies by ID using removePolicy', () => { + const pipeline = new PolicyPipeline(); + const policy1 = new UserAgentPolicy('Agent1', 'policy-1'); + const policy2 = new RetryPolicy(3, 200, 2000, 'policy-2'); + const policy3 = new UserAgentPolicy('Agent3', 'policy-3'); + + pipeline.usePolicy(policy1); + pipeline.usePolicy(policy2); + pipeline.usePolicy(policy3); + + pipeline.removePolicy('policy-2'); // Remove by ID string + + const policies = pipeline.listPolicies(); + expect(policies).toHaveLength(2); + expect(policies[0]).toBe(policy1); + expect(policies[1]).toBe(policy3); + }); + + it('removePolicy supports both policy instance and ID string', () => { + const pipeline = new PolicyPipeline(); + const policy1 = new UserAgentPolicy('Agent1', 'policy-1'); + const policy2 = new RetryPolicy(3, 200, 2000, 'policy-2'); + const policy3 = new UserAgentPolicy('Agent3', 'policy-3'); + const policy4 = new UserAgentPolicy('Agent4', 'policy-4'); + + pipeline.usePolicy(policy1); + pipeline.usePolicy(policy2); + pipeline.usePolicy(policy3); + pipeline.usePolicy(policy4); + + // Remove by policy instance + pipeline.removePolicy(policy2); + + // Remove by ID string + pipeline.removePolicy('policy-4'); + + const policies = pipeline.listPolicies(); + expect(policies).toHaveLength(2); + expect(policies[0]).toBe(policy1); + expect(policies[1]).toBe(policy3); + }); + + it('finds policies by ID', () => { + const pipeline = new PolicyPipeline(); + const policy1 = new UserAgentPolicy('Agent1', 'policy-1'); + const policy2 = new RetryPolicy(3, 200, 2000, 'policy-2'); + + pipeline.usePolicy(policy1); + pipeline.usePolicy(policy2); + + expect(pipeline.findPolicyById('policy-1')).toBe(policy1); + expect(pipeline.findPolicyById('policy-2')).toBe(policy2); + expect(pipeline.findPolicyById('non-existent')).toBeUndefined(); + }); + + it('fromVersionInfo accepts optional ID parameter', () => { + const versionInfo = { + name: 'test-app', + version: '1.0.0', + sha: 'abc123', + tag: 'v1.0.0', + branch: 'main' + }; + + const policyWithoutId = UserAgentPolicy.fromVersionInfo(versionInfo); + const policyWithId = UserAgentPolicy.fromVersionInfo( + versionInfo, + 'custom-id' + ); + + expect(policyWithoutId.id).toBeDefined(); + expect(policyWithId.id).toBe('custom-id'); + }); + }); +}); diff --git a/test/utils/httpPipelineUtils.ts b/test/utils/fetchRequestUtils.ts similarity index 62% rename from test/utils/httpPipelineUtils.ts rename to test/utils/fetchRequestUtils.ts index a8e7846..ec72a48 100644 --- a/test/utils/httpPipelineUtils.ts +++ b/test/utils/fetchRequestUtils.ts @@ -1,13 +1,13 @@ -// Copyright (c) Mapbox, Inc. -// Licensed under the MIT License. - import { expect, vi } from 'vitest'; import type { Mock } from 'vitest'; -import { HttpPipeline, UserAgentPolicy } from '../../src/utils/httpPipeline.js'; +import { + PolicyPipeline, + UserAgentPolicy +} from '../../src/utils/fetchRequest.js'; -export function setupHttpRequest(overrides?: Partial) { - const mockHttpRequest = vi.fn(); - mockHttpRequest.mockResolvedValue({ +export function setupFetch(overrides?: any) { + const mockFetch = vi.fn(); + mockFetch.mockResolvedValue({ ok: true, status: 200, statusText: 'OK', @@ -18,10 +18,10 @@ export function setupHttpRequest(overrides?: Partial) { // Build a real pipeline with UserAgentPolicy const userAgent = 'TestServer/1.0.0 (default, no-tag, abcdef)'; - const pipeline = new HttpPipeline(mockHttpRequest); + const pipeline = new PolicyPipeline(mockFetch); pipeline.usePolicy(new UserAgentPolicy(userAgent)); - return { httpRequest: pipeline.execute.bind(pipeline), mockHttpRequest }; + return { fetch: pipeline.fetch.bind(pipeline), mockFetch }; } export function assertHeadersSent(mockFetch: Mock) { diff --git a/test/utils/httpPipeline.test.ts b/test/utils/httpPipeline.test.ts deleted file mode 100644 index 1d52b56..0000000 --- a/test/utils/httpPipeline.test.ts +++ /dev/null @@ -1,268 +0,0 @@ -// Copyright (c) Mapbox, Inc. -// Licensed under the MIT License. - -import { describe, it, expect, vi, afterEach } from 'vitest'; -import { - RetryPolicy, - HttpPipeline, - UserAgentPolicy -} from '../../src/utils/httpPipeline.js'; -import type { Mock } from 'vitest'; - -function createMockFetch( - responses: Array<{ status: number; ok?: boolean }> -): typeof fetch { - let call = 0; - return vi.fn(async (_input: string | URL | Request, _init?: RequestInit) => { - const res = responses[Math.min(call, responses.length - 1)]; - call++; - return { - ok: res.ok ?? res.status < 400, - status: res.status, - statusText: `Status ${res.status}`, - json: async () => ({ status: res.status }) - } as Response; - }) as typeof fetch; -} - -describe('HttpPipeline', () => { - describe('RetryPolicy', () => { - afterEach(() => { - vi.restoreAllMocks(); - }); - - it('retries on 500 and returns last response after max retries', async () => { - const mockFetch = createMockFetch([ - { status: 500 }, - { status: 500 }, - { status: 500 }, - { status: 500 } - ]); - const pipeline = new HttpPipeline(mockFetch); - pipeline.usePolicy(new RetryPolicy(3, 1, 10)); // Use small delays for test speed - - const response = await pipeline.execute('http://test', {}); - - expect(mockFetch).toHaveBeenCalledTimes(4); - expect(response.status).toBe(500); - }); - - it('retries on 429 and succeeds if later response is ok', async () => { - const mockFetch = createMockFetch([ - { status: 429 }, - { status: 429 }, - { status: 200, ok: true } - ]); - const pipeline = new HttpPipeline(mockFetch); - pipeline.usePolicy(new RetryPolicy(3, 1, 10)); - - const response = await pipeline.execute('http://test', {}); - - expect(mockFetch).toHaveBeenCalledTimes(3); - expect(response.status).toBe(200); - expect(response.ok).toBe(true); - }); - - it('does not retry on 400 errors', async () => { - const mockFetch = createMockFetch([{ status: 400 }]); - const pipeline = new HttpPipeline(mockFetch); - pipeline.usePolicy(new RetryPolicy(3, 1, 10)); - - const response = await pipeline.execute('http://test', {}); - - expect(mockFetch).toHaveBeenCalledTimes(1); - expect(response.status).toBe(400); - }); - - it('returns immediately on first success', async () => { - const mockFetch = createMockFetch([{ status: 200, ok: true }]); - const pipeline = new HttpPipeline(mockFetch); - pipeline.usePolicy(new RetryPolicy(3, 1, 10)); - - const response = await pipeline.execute('http://test', {}); - - expect(mockFetch).toHaveBeenCalledTimes(1); - expect(response.status).toBe(200); - expect(response.ok).toBe(true); - }); - }); - - describe('UserAgentPolicy', () => { - it('sets the User-Agent header if not present', async () => { - const mockFetch = vi.fn( - async (input: string | URL | Request, init?: RequestInit) => { - return { - ok: true, - status: 200, - statusText: 'OK', - json: async () => ({}), - headers: init?.headers - } as Response; - } - ) as Mock; - - const pipeline = new HttpPipeline(mockFetch as unknown as typeof fetch); - pipeline.usePolicy(new UserAgentPolicy('TestAgent/1.0')); - - await pipeline.execute('http://test', {}); - - const headers = mockFetch.mock.calls[0][1]?.headers as Record< - string, - string - >; - expect(headers['User-Agent']).toBe('TestAgent/1.0'); - }); - - it('does not overwrite an existing User-Agent header', async () => { - const mockFetch = vi.fn( - async (input: string | URL | Request, init?: RequestInit) => { - return { - ok: true, - status: 200, - statusText: 'OK', - json: async () => ({}), - headers: init?.headers - } as Response; - } - ) as Mock; - - const pipeline = new HttpPipeline(mockFetch as unknown as typeof fetch); - pipeline.usePolicy(new UserAgentPolicy('TestAgent/1.0')); - - await pipeline.execute('http://test', { - headers: { - 'User-Agent': 'CustomAgent/2.0' - } - }); - - const headers = mockFetch.mock.calls[0][1]?.headers as Record< - string, - string - >; - expect(headers['User-Agent']).toBe('CustomAgent/2.0'); - }); - - it('works with headers as Headers object', async () => { - const mockFetch = vi.fn( - async (input: string | URL | Request, init?: RequestInit) => { - return { - ok: true, - status: 200, - statusText: 'OK', - json: async () => ({}), - headers: init?.headers - } as Response; - } - ) as Mock; - - const pipeline = new HttpPipeline(mockFetch as unknown as typeof fetch); - pipeline.usePolicy(new UserAgentPolicy('TestAgent/1.0')); - - const headers = new Headers(); - await pipeline.execute('http://test', { headers }); - - expect(headers.get('User-Agent')).toBe('TestAgent/1.0'); - }); - }); - - describe('Policy Management', () => { - it('can add and list policies', () => { - const mockFetch = vi.fn(); - const pipeline = new HttpPipeline(mockFetch as unknown as typeof fetch); - - const userAgentPolicy = new UserAgentPolicy( - 'TestAgent/1.0', - 'user-agent-test' - ); - const retryPolicy = new RetryPolicy(3, 100, 1000, 'retry-test'); - - pipeline.usePolicy(userAgentPolicy); - pipeline.usePolicy(retryPolicy); - - const policies = pipeline.listPolicies(); - expect(policies).toHaveLength(2); - expect(policies[0].id).toBe('user-agent-test'); - expect(policies[1].id).toBe('retry-test'); - }); - - it('can find policy by ID', () => { - const mockFetch = vi.fn(); - const pipeline = new HttpPipeline(mockFetch as unknown as typeof fetch); - - const userAgentPolicy = new UserAgentPolicy( - 'TestAgent/1.0', - 'user-agent-test' - ); - pipeline.usePolicy(userAgentPolicy); - - const foundPolicy = pipeline.findPolicyById('user-agent-test'); - expect(foundPolicy).toBe(userAgentPolicy); - - const notFoundPolicy = pipeline.findPolicyById('non-existent'); - expect(notFoundPolicy).toBeUndefined(); - }); - - it('can remove policy by ID', () => { - const mockFetch = vi.fn(); - const pipeline = new HttpPipeline(mockFetch as unknown as typeof fetch); - - const userAgentPolicy = new UserAgentPolicy( - 'TestAgent/1.0', - 'user-agent-test' - ); - const retryPolicy = new RetryPolicy(3, 100, 1000, 'retry-test'); - - pipeline.usePolicy(userAgentPolicy); - pipeline.usePolicy(retryPolicy); - - expect(pipeline.listPolicies()).toHaveLength(2); - - pipeline.removePolicy('user-agent-test'); - - const policies = pipeline.listPolicies(); - expect(policies).toHaveLength(1); - expect(policies[0].id).toBe('retry-test'); - }); - - it('can remove policy by reference', () => { - const mockFetch = vi.fn(); - const pipeline = new HttpPipeline(mockFetch as unknown as typeof fetch); - - const userAgentPolicy = new UserAgentPolicy( - 'TestAgent/1.0', - 'user-agent-test' - ); - const retryPolicy = new RetryPolicy(3, 100, 1000, 'retry-test'); - - pipeline.usePolicy(userAgentPolicy); - pipeline.usePolicy(retryPolicy); - - expect(pipeline.listPolicies()).toHaveLength(2); - - pipeline.removePolicy(userAgentPolicy); - - const policies = pipeline.listPolicies(); - expect(policies).toHaveLength(1); - expect(policies[0].id).toBe('retry-test'); - }); - - it('generates automatic IDs for policies without explicit ID', () => { - const userAgentPolicy = new UserAgentPolicy('TestAgent/1.0'); - const retryPolicy = new RetryPolicy(3, 100, 1000); - - expect(userAgentPolicy.id).toMatch(/^user-agent-\d+-[a-z0-9]+$/); - expect(retryPolicy.id).toMatch(/^retry-\d+-[a-z0-9]+$/); - }); - - it('uses provided IDs when specified', () => { - const userAgentPolicy = new UserAgentPolicy( - 'TestAgent/1.0', - 'custom-ua-id' - ); - const retryPolicy = new RetryPolicy(3, 100, 1000, 'custom-retry-id'); - - expect(userAgentPolicy.id).toBe('custom-ua-id'); - expect(retryPolicy.id).toBe('custom-retry-id'); - }); - }); -}); diff --git a/test/utils/jwtUtils.test.ts b/test/utils/jwtUtils.test.ts deleted file mode 100644 index 5398815..0000000 --- a/test/utils/jwtUtils.test.ts +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright (c) Mapbox, Inc. -// Licensed under the MIT License. - -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import * as jwtUtils from '../../src/utils/jwtUtils.js'; - -describe('jwtUtils', () => { - describe('getUserNameFromToken', () => { - beforeEach(() => { - vi.unstubAllEnvs(); - }); - - it('extracts username from valid token', () => { - const testPayload = Buffer.from( - JSON.stringify({ u: 'myusername' }) - ).toString('base64'); - - vi.stubEnv( - 'MAPBOX_ACCESS_TOKEN', - `eyJhbGciOiJIUzI1NiJ9.${testPayload}.signature` - ); - - const username = jwtUtils.getUserNameFromToken(); - expect(username).toBe('myusername'); - }); - - it('throws error when token is not set', () => { - vi.stubEnv('MAPBOX_ACCESS_TOKEN', ''); - - expect(() => jwtUtils.getUserNameFromToken()).toThrow( - 'No access token provided. Please set MAPBOX_ACCESS_TOKEN environment variable or pass it as an argument.' - ); - }); - - it('throws error when token has invalid format', () => { - vi.stubEnv('MAPBOX_ACCESS_TOKEN', 'invalid-token-format'); - - expect(() => jwtUtils.getUserNameFromToken()).toThrow( - 'MAPBOX_ACCESS_TOKEN is not in valid JWT format' - ); - }); - - it('throws error when payload does not contain username', () => { - const invalidPayload = Buffer.from( - JSON.stringify({ sub: 'test' }) - ).toString('base64'); - - vi.stubEnv( - 'MAPBOX_ACCESS_TOKEN', - `eyJhbGciOiJIUzI1NiJ9.${invalidPayload}.signature` - ); - - expect(() => jwtUtils.getUserNameFromToken()).toThrow( - 'MAPBOX_ACCESS_TOKEN does not contain username in payload' - ); - }); - }); - - describe('username extraction from token', () => { - it('throws error for invalid JWT format', () => { - vi.stubEnv('MAPBOX_ACCESS_TOKEN', 'invalid-token'); - - expect(() => { - jwtUtils.getUserNameFromToken(); - }).toThrow('MAPBOX_ACCESS_TOKEN is not in valid JWT format'); - }); - - it('throws error when username field is missing', () => { - const tokenWithoutUsername = - 'eyJhbGciOiJIUzI1NiJ9.eyJhIjoidGVzdC1hcGkifQ.signature'; - - vi.stubEnv('MAPBOX_ACCESS_TOKEN', tokenWithoutUsername); - - expect(() => { - jwtUtils.getUserNameFromToken(); - }).toThrow('MAPBOX_ACCESS_TOKEN does not contain username in payload'); - }); - }); -});