diff --git a/CHANGELOG.md b/CHANGELOG.md index a740d3a..7d785c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## Unreleased +### Dependencies + +- **Upgrade `zod` from `^3.25.42` to `^4.3.6`** — migrated all `z.record()` calls to require explicit key schema (`z.string()`), updated test assertions for changed error message format + ## 0.6.0 - 2026-04-01 ### Security diff --git a/package-lock.json b/package-lock.json index 83d4acf..bc9c6df 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,7 @@ "@opentelemetry/sdk-node": "^0.56.0", "@opentelemetry/sdk-trace-base": "^1.30.1", "@opentelemetry/semantic-conventions": "^1.30.1", - "zod": "^3.25.42" + "zod": "^4.3.6" }, "bin": { "mapbox-mcp-devkit": "dist/esm/index.js" @@ -13854,7 +13854,9 @@ } }, "node_modules/zod": { - "version": "3.25.71", + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/package.json b/package.json index 1732dc7..f9c5e96 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "@opentelemetry/sdk-node": "^0.56.0", "@opentelemetry/sdk-trace-base": "^1.30.1", "@opentelemetry/semantic-conventions": "^1.30.1", - "zod": "^3.25.42" + "zod": "^4.3.6" }, "devDependencies": { "@eslint/js": "^9.27.0", diff --git a/src/schemas/style.ts b/src/schemas/style.ts index 88fcb0e..bd936a2 100644 --- a/src/schemas/style.ts +++ b/src/schemas/style.ts @@ -28,7 +28,7 @@ const LightsSchema = z.array( z.object({ id: z.string(), type: z.enum(['ambient', 'directional']), - properties: z.record(z.any()).optional() + properties: z.record(z.string(), z.any()).optional() }) ); @@ -48,7 +48,7 @@ const VectorSourceSchema = z.object({ 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(), + promoteId: z.union([z.string(), z.record(z.string(), z.string())]).optional(), volatile: z.boolean().optional() }); @@ -88,10 +88,10 @@ const GeoJSONSourceSchema = z.object({ cluster: z.boolean().optional(), clusterRadius: z.number().optional(), clusterMaxZoom: z.number().optional(), - clusterProperties: z.record(z.any()).optional(), + clusterProperties: z.record(z.string(), z.any()).optional(), lineMetrics: z.boolean().optional(), generateId: z.boolean().optional(), - promoteId: z.union([z.string(), z.record(z.string())]).optional() + promoteId: z.union([z.string(), z.record(z.string(), z.string())]).optional() }); const ImageSourceSchema = z.object({ @@ -156,9 +156,9 @@ const LayerSchema = z.object({ 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(), + layout: z.record(z.string(), z.any()).optional(), + paint: z.record(z.string(), z.any()).optional(), + metadata: z.record(z.string(), z.any()).optional(), slot: z.string().optional().describe('Slot this layer is assigned to') }); @@ -166,7 +166,7 @@ const LayerSchema = z.object({ const StyleImportSchema = z.object({ id: z.string(), url: z.string(), - config: z.record(z.any()).optional() + config: z.record(z.string(), z.any()).optional() }); // Base Style properties (shared between input and output) @@ -175,12 +175,14 @@ export const BaseStylePropertiesSchema = z.object({ version: z .literal(8) .describe('Style specification version number. Must be 8'), - sources: z.record(SourceSchema).describe('Data source specifications'), + sources: z + .record(z.string(), SourceSchema) + .describe('Data source specifications'), layers: z.array(LayerSchema).describe('Layers in draw order'), // Optional Style Spec properties metadata: z - .record(z.any()) + .record(z.string(), z.any()) .optional() .describe('Arbitrary properties for tracking'), center: CoordinatesSchema.optional().describe( @@ -203,9 +205,13 @@ export const BaseStylePropertiesSchema = z.object({ terrain: TerrainSchema.optional() .nullable() .describe('Global terrain elevation'), - fog: z.record(z.any()).optional().nullable().describe('Fog properties'), + fog: z + .record(z.string(), z.any()) + .optional() + .nullable() + .describe('Fog properties'), projection: z - .record(z.any()) + .record(z.string(), z.any()) .optional() .nullable() .describe('Map projection'), diff --git a/src/tools/compare-styles-tool/CompareStylesTool.input.schema.ts b/src/tools/compare-styles-tool/CompareStylesTool.input.schema.ts index 673b9cf..0ea7d15 100644 --- a/src/tools/compare-styles-tool/CompareStylesTool.input.schema.ts +++ b/src/tools/compare-styles-tool/CompareStylesTool.input.schema.ts @@ -5,10 +5,10 @@ import { z } from 'zod'; export const CompareStylesInputSchema = z.object({ styleA: z - .union([z.string(), z.record(z.unknown())]) + .union([z.string(), z.record(z.string(), z.unknown())]) .describe('First Mapbox style (JSON string or style object)'), styleB: z - .union([z.string(), z.record(z.unknown())]) + .union([z.string(), z.record(z.string(), z.unknown())]) .describe('Second Mapbox style (JSON string or style object)'), ignoreMetadata: z .boolean() diff --git a/src/tools/create-style-tool/CreateStyleTool.input.schema.ts b/src/tools/create-style-tool/CreateStyleTool.input.schema.ts index a20c5f2..ba7f950 100644 --- a/src/tools/create-style-tool/CreateStyleTool.input.schema.ts +++ b/src/tools/create-style-tool/CreateStyleTool.input.schema.ts @@ -9,7 +9,7 @@ import { z } from 'zod'; export const CreateStyleInputSchema = z.object({ name: z.string().describe('Human-readable name for the style'), style: z - .record(z.any()) + .record(z.string(), z.any()) .describe( 'Complete Mapbox Style Specification object. Must include: version (8), sources, layers. Optional: sprite, glyphs, center, zoom, bearing, pitch, metadata, etc. See https://docs.mapbox.com/mapbox-gl-js/style-spec/' ) diff --git a/src/tools/list-styles-tool/ListStylesTool.output.schema.ts b/src/tools/list-styles-tool/ListStylesTool.output.schema.ts index 3c73b6d..296cd42 100644 --- a/src/tools/list-styles-tool/ListStylesTool.output.schema.ts +++ b/src/tools/list-styles-tool/ListStylesTool.output.schema.ts @@ -30,7 +30,10 @@ const StyleMetadataSchema = z.object({ 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'), + sources: z + .record(z.string(), z.any()) + .optional() + .describe('Style data sources'), layers: z.array(z.any()).optional().describe('Style layers'), // Additional metadata fields diff --git a/src/tools/optimize-style-tool/OptimizeStyleTool.input.schema.ts b/src/tools/optimize-style-tool/OptimizeStyleTool.input.schema.ts index e9271ed..a385c80 100644 --- a/src/tools/optimize-style-tool/OptimizeStyleTool.input.schema.ts +++ b/src/tools/optimize-style-tool/OptimizeStyleTool.input.schema.ts @@ -5,7 +5,7 @@ import { z } from 'zod'; export const OptimizeStyleInputSchema = z.object({ style: z - .union([z.string(), z.record(z.unknown())]) + .union([z.string(), z.record(z.string(), z.unknown())]) .describe('Mapbox style to optimize (JSON string or style object)'), optimizations: z .array( diff --git a/src/tools/optimize-style-tool/OptimizeStyleTool.output.schema.ts b/src/tools/optimize-style-tool/OptimizeStyleTool.output.schema.ts index 7142f29..f31cbf9 100644 --- a/src/tools/optimize-style-tool/OptimizeStyleTool.output.schema.ts +++ b/src/tools/optimize-style-tool/OptimizeStyleTool.output.schema.ts @@ -10,7 +10,9 @@ const OptimizationSchema = z.object({ }); export const OptimizeStyleOutputSchema = z.object({ - optimizedStyle: z.record(z.unknown()).describe('The optimized Mapbox style'), + optimizedStyle: z + .record(z.string(), z.unknown()) + .describe('The optimized Mapbox style'), optimizations: z .array(OptimizationSchema) .describe('List of optimizations that were applied'), diff --git a/src/tools/style-builder-tool/StyleBuilderTool.input.schema.ts b/src/tools/style-builder-tool/StyleBuilderTool.input.schema.ts index 0d34073..a365dca 100644 --- a/src/tools/style-builder-tool/StyleBuilderTool.input.schema.ts +++ b/src/tools/style-builder-tool/StyleBuilderTool.input.schema.ts @@ -52,7 +52,7 @@ const LayerConfigSchema = z.object({ z.number(), z.boolean(), z.array(z.unknown()), - z.record(z.unknown()) + z.record(z.string(), z.unknown()) ]) .optional() .describe('Custom filter expression'), @@ -109,7 +109,7 @@ const LayerConfigSchema = z.object({ z.number(), z.boolean(), z.array(z.unknown()), - z.record(z.unknown()) + z.record(z.string(), z.unknown()) ]) .optional() .describe('Custom Mapbox expression for advanced styling'), diff --git a/src/tools/tilequery-tool/TilequeryTool.output.schema.ts b/src/tools/tilequery-tool/TilequeryTool.output.schema.ts index e46923e..4c99b62 100644 --- a/src/tools/tilequery-tool/TilequeryTool.output.schema.ts +++ b/src/tools/tilequery-tool/TilequeryTool.output.schema.ts @@ -32,7 +32,7 @@ const VectorTilequeryFeatureSchema = z.object({ .describe('The vector tile layer of the feature result') }) }) - .and(z.record(z.any())) // Allow additional properties from the original feature + .and(z.record(z.string(), z.any())) // Allow additional properties from the original feature }); // Rasterarray Tileset Feature Schema diff --git a/src/tools/update-style-tool/UpdateStyleTool.input.schema.ts b/src/tools/update-style-tool/UpdateStyleTool.input.schema.ts index ef1dcbf..b75dc8e 100644 --- a/src/tools/update-style-tool/UpdateStyleTool.input.schema.ts +++ b/src/tools/update-style-tool/UpdateStyleTool.input.schema.ts @@ -9,7 +9,7 @@ export const UpdateStyleInputSchema = 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()) + .record(z.string(), z.any()) .optional() .describe( 'Complete Mapbox Style Specification object to update. Must include: version (8), sources, layers. Optional: sprite, glyphs, center, zoom, bearing, pitch, metadata, etc. See https://docs.mapbox.com/mapbox-gl-js/style-spec/' diff --git a/src/tools/validate-geojson-tool/ValidateGeojsonTool.input.schema.ts b/src/tools/validate-geojson-tool/ValidateGeojsonTool.input.schema.ts index cb635ad..84fb26b 100644 --- a/src/tools/validate-geojson-tool/ValidateGeojsonTool.input.schema.ts +++ b/src/tools/validate-geojson-tool/ValidateGeojsonTool.input.schema.ts @@ -5,7 +5,7 @@ import { z } from 'zod'; export const ValidateGeojsonInputSchema = z.object({ geojson: z - .union([z.string(), z.record(z.unknown())]) + .union([z.string(), z.record(z.string(), z.unknown())]) .describe('GeoJSON object or JSON string to validate') }); diff --git a/src/tools/validate-style-tool/ValidateStyleTool.input.schema.ts b/src/tools/validate-style-tool/ValidateStyleTool.input.schema.ts index 21522f0..341542c 100644 --- a/src/tools/validate-style-tool/ValidateStyleTool.input.schema.ts +++ b/src/tools/validate-style-tool/ValidateStyleTool.input.schema.ts @@ -9,7 +9,7 @@ import { z } from 'zod'; */ export const ValidateStyleInputSchema = z.object({ style: z - .union([z.string(), z.record(z.unknown())]) + .union([z.string(), z.record(z.string(), z.unknown())]) .describe( 'Mapbox style JSON object or JSON string to validate against the Mapbox Style Specification' ) diff --git a/test/tools/create-token-tool/CreateTokenTool.test.ts b/test/tools/create-token-tool/CreateTokenTool.test.ts index 5e336dd..1542202 100644 --- a/test/tools/create-token-tool/CreateTokenTool.test.ts +++ b/test/tools/create-token-tool/CreateTokenTool.test.ts @@ -60,7 +60,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('Required'); + expect(errorText).toContain('invalid_type'); }); it('validates allowedUrls array length', async () => { @@ -93,7 +93,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('Invalid enum value'); + expect(errorText).toContain('invalid_value'); }); it('throws error when unable to extract username from token', async () => { diff --git a/test/tools/get-feedback-tool/GetFeedbackTool.test.ts b/test/tools/get-feedback-tool/GetFeedbackTool.test.ts index ea4d12b..00ef244 100644 --- a/test/tools/get-feedback-tool/GetFeedbackTool.test.ts +++ b/test/tools/get-feedback-tool/GetFeedbackTool.test.ts @@ -256,7 +256,7 @@ describe('GetFeedbackTool', () => { // Should fail validation expect(result.isError).toBe(true); - expect(result.content[0].text).toContain('Required'); + expect(result.content[0].text).toContain('invalid_type'); expect(result.content[0].text).toContain('feedback_id'); }); }); diff --git a/test/tools/list-tokens-tool/ListTokensTool.test.ts b/test/tools/list-tokens-tool/ListTokensTool.test.ts index cfd5aad..1af785c 100644 --- a/test/tools/list-tokens-tool/ListTokensTool.test.ts +++ b/test/tools/list-tokens-tool/ListTokensTool.test.ts @@ -59,7 +59,7 @@ describe('ListTokensTool', () => { expect(result.isError).toBe(true); expect(result.content[0]).toHaveProperty('type', 'text'); const errorText = (result.content[0] as TextContent).text; - expect(errorText).toContain('Number must be less than or equal to 100'); + expect(errorText).toContain('<=100'); }); it('validates sortby enum values', async () => { diff --git a/test/tools/style-comparison-tool/StyleComparisonTool.test.ts b/test/tools/style-comparison-tool/StyleComparisonTool.test.ts index 78f238f..e715011 100644 --- a/test/tools/style-comparison-tool/StyleComparisonTool.test.ts +++ b/test/tools/style-comparison-tool/StyleComparisonTool.test.ts @@ -62,7 +62,7 @@ describe('StyleComparisonTool', () => { expect(result.isError).toBe(true); expect( (result.content[0] as { type: 'text'; text: string }).text - ).toContain('Required'); + ).toContain('invalid_type'); }); it('should handle full style URLs', async () => {