Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
201 changes: 201 additions & 0 deletions src/schemas/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
import { z } from 'zod';

/**
* Common Zod schema definitions for reuse across different tools
*/

/**
* Public Mapbox access token that starts with 'pk.'
*/
export const publicAccessTokenSchema = (description?: string) =>
z
.string()
.startsWith(
'pk.',
'Invalid access token. Only public tokens (starting with pk.*) are allowed. Secret tokens (sk.*) cannot be used as they cannot be exposed in browser URLs.'
)
.describe(
description ||
'Mapbox public access token (required, must start with pk.* and have styles:read permission). Secret tokens (sk.*) cannot be used as they cannot be exposed in browser URLs. Please use an existing public token or get one from list_tokens_tool or create one with create_token_tool with styles:read permission.'
);

/**
* Latitude coordinate with validation
*/
export const latitudeSchema = (
description?: string,
optional: boolean = false
) =>
numberSchema(
-90,
90,
description || 'Latitude coordinate (-90 to 90)',
optional
);

/**
* Longitude coordinate with validation
*/
export const longitudeSchema = (
description?: string,
optional: boolean = false
) =>
numberSchema(
-180,
180,
description || 'Longitude coordinate (-180 to 180)',
optional
);

/**
* Zoom level for map views
*/
export const zoomSchema = (description?: string) =>
z
.number()
.optional()
.describe(
description ||
'Initial zoom level for the map view (0-22). If provided along with latitude and longitude, sets the initial map position.'
);

/**
* Complete coordinate set (latitude + longitude + zoom) for map positioning
*/
export const mapPositionSchema = (
latDescription?: string,
lngDescription?: string,
zoomDescription?: string
) => ({
latitude: latitudeSchema(
latDescription ||
'Latitude coordinate for the initial map center (-90 to 90). Must be provided together with longitude and zoom.',
true
),
longitude: longitudeSchema(
lngDescription ||
'Longitude coordinate for the initial map center (-180 to 180). Must be provided together with latitude and zoom.',
true
),
zoom: zoomSchema(zoomDescription)
});

/**
* Generic string schema
*/
export const stringSchema = (
description: string,
optional: boolean = false
) => {
const schema = z.string().describe(description);
return optional ? schema.optional() : schema;
};

/**
* Generic number schema with range validation
*/
export const numberSchema = (
min?: number,
max?: number,
description?: string,
optional: boolean = false
) => {
let schema = z.number();

if (min !== undefined) {
schema = schema.min(min);
}
if (max !== undefined) {
schema = schema.max(max);
}

schema = schema.describe(
description ||
`Number${min !== undefined ? ` (min: ${min}` : ''}${max !== undefined ? `${min !== undefined ? ', ' : ' ('}max: ${max}` : ''}${min !== undefined || max !== undefined ? ')' : ''}`
);

return optional ? schema.optional() : schema;
};

/**
* Generic boolean schema
*/
export const booleanSchema = (
description: string,
optional: boolean = false,
defaultValue?: boolean
) => {
const baseSchema = z.boolean().describe(description);

if (defaultValue !== undefined) {
return baseSchema.default(defaultValue);
}

if (optional) {
return baseSchema.optional();
}

return baseSchema;
};

/**
* Generic enum schema
*/
export const enumSchema = <T extends readonly [string, ...string[]]>(
values: T,
description: string,
optional: boolean = false
) => {
const schema = z.enum(values).describe(description);
return optional ? schema.optional() : schema;
};

/**
* Generic array schema
*/
export const arraySchema = <T extends z.ZodTypeAny>(
itemSchema: T,
description: string,
optional: boolean = false
) => {
const schema = z.array(itemSchema).describe(description);
return optional ? schema.optional() : schema;
};

/**
* Generic record/object schema
*/
export const recordSchema = (
description?: string,
optional: boolean = false
) => {
const schema = z
.record(z.any())
.describe(description || 'Object with arbitrary key-value pairs');

return optional ? schema.optional() : schema;
};

/**
* Mapbox style specification object
*/
export const mapboxStyleSchema = (
description?: string,
optional: boolean = false
) => recordSchema(description || 'Mapbox style specification object', optional);

/**
* Pagination limit field
*/
export const limitSchema = (
min: number = 1,
max: number = 100,
description?: string,
optional: boolean = true
) =>
numberSchema(
min,
max,
description || `Maximum number of items to return (${min}-${max})`,
optional
);
5 changes: 3 additions & 2 deletions src/tools/create-style-tool/CreateStyleTool.schema.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { z } from 'zod';
import { stringSchema, mapboxStyleSchema } from '../../schemas/common.js';

export const CreateStyleSchema = z.object({
name: z.string().describe('Name for the new style'),
style: z.record(z.any()).describe('Mapbox style specification object')
name: stringSchema('New name for the style'),
style: mapboxStyleSchema()
});

export type CreateStyleInput = z.infer<typeof CreateStyleSchema>;
31 changes: 15 additions & 16 deletions src/tools/create-token-tool/CreateTokenTool.schema.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { z } from 'zod';
import { stringSchema, arraySchema } from '../../schemas/common.js';

// Public scopes that can be used with public tokens
const PUBLIC_SCOPES = [
Expand Down Expand Up @@ -44,22 +45,20 @@ const SECRET_SCOPES = [
const ALL_SCOPES = [...PUBLIC_SCOPES, ...SECRET_SCOPES] as const;

export const CreateTokenSchema = z.object({
note: z.string().describe('Description of the token'),
scopes: z
.array(z.enum(ALL_SCOPES))
.describe(
'Array of scopes/permissions for the token. PUBLIC scopes (styles:tiles, styles:read, fonts:read, datasets:read, vision:read) create a public token. SECRET scopes (all others) create a secret token. If any secret scope is included, the entire token becomes secret and will only be visible once upon creation.'
),
allowedUrls: z
.array(z.string())
.optional()
.describe('Optional array of URLs where the token can be used (max 100)'),
expires: z
.string()
.optional()
.describe(
'Optional expiration time in ISO 8601 format (maximum 1 hour in the future)'
)
note: stringSchema('Description of the token'),
scopes: arraySchema(
z.enum(ALL_SCOPES),
'Array of scopes/permissions for the token. PUBLIC scopes (styles:tiles, styles:read, fonts:read, datasets:read, vision:read) create a public token. SECRET scopes (all others) create a secret token. If any secret scope is included, the entire token becomes secret and will only be visible once upon creation.'
),
allowedUrls: arraySchema(
z.string(),
'Optional array of URLs where the token can be used (max 100)',
true
),
expires: stringSchema(
'Optional expiration time in ISO 8601 format (maximum 1 hour in the future)',
true
)
});

export type CreateTokenInput = z.infer<typeof CreateTokenSchema>;
Expand Down
6 changes: 3 additions & 3 deletions src/tools/create-token-tool/CreateTokenTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export class CreateTokenTool extends MapboxApiBasedTool<
);

// Check if any secret scopes are being used
const hasSecretScopes = input.scopes.some((scope) =>
const hasSecretScopes = input.scopes?.some((scope) =>
SECRET_SCOPES.includes(scope as (typeof SECRET_SCOPES)[number])
);

Expand All @@ -52,8 +52,8 @@ export class CreateTokenTool extends MapboxApiBasedTool<
allowedUrls?: string[];
expires?: string;
} = {
note: input.note,
scopes: input.scopes
note: input.note as string,
scopes: input.scopes || []
};

if (input.allowedUrls) {
Expand Down
3 changes: 2 additions & 1 deletion src/tools/delete-style-tool/DeleteStyleTool.schema.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { z } from 'zod';
import { stringSchema } from '../../schemas/common.js';

export const DeleteStyleSchema = z.object({
styleId: z.string().describe('Style ID to delete')
styleId: stringSchema('Style ID to delete')
});

export type DeleteStyleInput = z.infer<typeof DeleteStyleSchema>;
22 changes: 10 additions & 12 deletions src/tools/list-styles-tool/ListStylesTool.schema.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
import { z } from 'zod';
import { limitSchema, stringSchema } from '../../schemas/common.js';

export const ListStylesSchema = z.object({
limit: z
.number()
.optional()
.describe(
'Maximum number of styles to return (recommended: 5-10 to avoid token limits, default: no limit)'
),
start: z
.string()
.optional()
.describe(
'Start token for pagination (use the "start" value from previous response)'
)
limit: limitSchema(
1,
500,
'Maximum number of styles to return (recommended: 5-10 to avoid token limits, default: no limit)'
),
start: stringSchema(
'Start token for pagination (use the "start" value from previous response)',
true
)
});

export type ListStylesInput = z.infer<typeof ListStylesSchema>;
38 changes: 19 additions & 19 deletions src/tools/list-tokens-tool/ListTokensTool.schema.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
import { z } from 'zod';
import {
limitSchema,
stringSchema,
booleanSchema,
enumSchema
} from '../../schemas/common.js';

export const ListTokensSchema = z.object({
default: z
.boolean()
.optional()
.describe('Filter to show only the default public token'),
limit: z
.number()
.min(1)
.max(100)
.optional()
.describe('Maximum number of tokens to return (1-100)'),
sortby: z
.enum(['created', 'modified'])
.optional()
.describe('Sort tokens by created or modified timestamp'),
start: z.string().optional().describe('Token ID to start pagination from'),
usage: z
.enum(['pk', 'sk', 'tk'])
.optional()
.describe('Filter by token type: pk (public), sk (secret), tk (temporary)')
default: booleanSchema('Filter to show only the default public token', true),
limit: limitSchema(1, 100, 'Maximum number of tokens to return (1-100)'),
sortby: enumSchema(
['created', 'modified'],
'Sort tokens by created or modified timestamp',
true
),
start: stringSchema('Token ID to start pagination from', true),
usage: enumSchema(
['pk', 'sk', 'tk'],
'Filter by token type: pk (public), sk (secret), tk (temporary)',
true
)
});

export type ListTokensInput = z.infer<typeof ListTokensSchema>;
29 changes: 9 additions & 20 deletions src/tools/preview-style-tool/PreviewStyleTool.schema.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,15 @@
import { z } from 'zod';
import {
publicAccessTokenSchema,
stringSchema,
booleanSchema
} from '../../schemas/common.js';

export const PreviewStyleSchema = z.object({
styleId: z.string().describe('Style ID to preview'),
accessToken: z
.string()
.startsWith(
'pk.',
'Invalid access token. Only public tokens (starting with pk.*) are allowed for preview URLs. Secret tokens (sk.*) cannot be used as they cannot be exposed in browser URLs.'
)
.describe(
'Mapbox public access token (required, must start with pk.* and have styles:read permission). Secret tokens (sk.*) cannot be used as they cannot be exposed in browser URLs. Please use an existing public token or get one from list_tokens_tool or create one with create_token_tool with styles:read permission.'
),
title: z
.boolean()
.optional()
.default(false)
.describe('Show title in the preview'),
zoomwheel: z
.boolean()
.optional()
.default(true)
.describe('Enable zoom wheel control')
styleId: stringSchema('Style ID to preview'),
accessToken: publicAccessTokenSchema(),
title: booleanSchema('Show title in the preview', true, false),
zoomwheel: booleanSchema('Enable zoom wheel control', true, true)
});

export type PreviewStyleInput = z.infer<typeof PreviewStyleSchema>;
Loading
Loading