Skip to content

Commit 7f423a3

Browse files
[tools] Update tools to use structuredContent with schema
1 parent 446bb2f commit 7f423a3

11 files changed

Lines changed: 296 additions & 275 deletions

File tree

src/schemas/style.ts

Lines changed: 183 additions & 214 deletions
Large diffs are not rendered by default.

src/tools/BaseTool.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ export abstract class BaseTool<
8383
// Add outputSchema if provided
8484
if (this.outputSchema) {
8585
// Pass the schema shape directly - don't wrap
86-
// The MCP SDK will validate structuredContent.data against this schema
86+
// The MCP SDK will validate structuredContent against this schema
8787
config.outputSchema =
8888
// eslint-disable-next-line @typescript-eslint/no-explicit-any
8989
(this.outputSchema as unknown as z.ZodObject<any>).shape;

src/tools/create-token-tool/CreateTokenTool.output.schema.ts

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,28 @@
44
import { z } from 'zod';
55

66
export const CreateTokenOutputSchema = z.object({
7-
id: z.string().describe('Token ID'),
8-
scopes: z.array(z.string()).describe('Array of scopes assigned to the token'),
9-
token: z.string().describe('The actual token string'),
7+
id: z.string().describe("The token's unique identifier"),
8+
usage: z
9+
.enum(['pk', 'sk', 'tk'])
10+
.describe('Token usage type: pk (public), sk (secret), or tk (temporary)'),
11+
client: z.string().describe('The client for the token'),
12+
default: z.boolean().describe('Whether this is the default token'),
13+
scopes: z.array(z.string()).describe('Array of scopes granted to the token'),
14+
note: z
15+
.string()
16+
.nullable()
17+
.describe('Human-readable description of the token'),
1018
created: z.string().describe('ISO 8601 creation timestamp'),
1119
modified: z.string().describe('ISO 8601 last modified timestamp'),
12-
usage: z.string().describe('Token usage type, e.g. pk or sk'),
13-
default: z.boolean().describe('Whether this is the default token'),
14-
note: z.string().optional().describe('Optional note or description'),
15-
allowedUrls: z.array(z.string()).optional().describe('Array of allowed URLs'),
16-
expires: z.string().optional().describe('Expiration time in ISO 8601 format'),
17-
message: z.string().optional().describe('Status or error message')
20+
allowedUrls: z
21+
.array(z.string())
22+
.optional()
23+
.describe('URLs that the token is restricted to'),
24+
token: z.string().describe('The actual access token string'),
25+
expires: z
26+
.string()
27+
.optional()
28+
.describe('Expiration time in ISO 8601 format (temporary tokens only)')
1829
});
1930

2031
export type CreateTokenOutput = z.infer<typeof CreateTokenOutputSchema>;

src/tools/list-styles-tool/ListStylesTool.output.schema.ts

Lines changed: 29 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -8,37 +8,35 @@ import { z } from 'zod';
88
* Note: This is different from a full style specification - it contains
99
* metadata about the style but may not include all style properties like layers.
1010
*/
11-
const StyleMetadataSchema = z
12-
.object({
13-
// Core metadata fields always present
14-
id: z.string().describe('Unique style ID'),
15-
name: z.string().describe('Style name'),
16-
owner: z.string().describe('Username of the style owner'),
17-
created: z.string().describe('ISO 8601 timestamp of creation'),
18-
modified: z.string().describe('ISO 8601 timestamp of last modification'),
19-
visibility: z
20-
.enum(['public', 'private'])
21-
.describe('Style visibility setting'),
22-
23-
// Optional Style Spec fields that may be included
24-
version: z.literal(8).optional().describe('Style specification version'),
25-
center: z
26-
.tuple([z.number(), z.number()])
27-
.optional()
28-
.describe('Default center [longitude, latitude]'),
29-
zoom: z.number().optional().describe('Default zoom level'),
30-
bearing: z.number().optional().describe('Default bearing in degrees'),
31-
pitch: z.number().optional().describe('Default pitch in degrees'),
32-
33-
// Sources and layers may or may not be included in list responses
34-
sources: z.record(z.any()).optional().describe('Style data sources'),
35-
layers: z.array(z.any()).optional().describe('Style layers'),
36-
37-
// Additional metadata fields
38-
protected: z.boolean().optional().describe('Whether style is protected'),
39-
draft: z.boolean().optional().describe('Whether style is a draft')
40-
})
41-
.passthrough(); // Allow additional fields from API
11+
const StyleMetadataSchema = z.object({
12+
// Core metadata fields always present
13+
id: z.string().describe('Unique style ID'),
14+
name: z.string().describe('Style name'),
15+
owner: z.string().describe('Username of the style owner'),
16+
created: z.string().describe('ISO 8601 timestamp of creation'),
17+
modified: z.string().describe('ISO 8601 timestamp of last modification'),
18+
visibility: z
19+
.enum(['public', 'private'])
20+
.describe('Style visibility setting'),
21+
22+
// Optional Style Spec fields that may be included
23+
version: z.literal(8).optional().describe('Style specification version'),
24+
center: z
25+
.tuple([z.number(), z.number()])
26+
.optional()
27+
.describe('Default center [longitude, latitude]'),
28+
zoom: z.number().optional().describe('Default zoom level'),
29+
bearing: z.number().optional().describe('Default bearing in degrees'),
30+
pitch: z.number().optional().describe('Default pitch in degrees'),
31+
32+
// Sources and layers may or may not be included in list responses
33+
sources: z.record(z.any()).optional().describe('Style data sources'),
34+
layers: z.array(z.any()).optional().describe('Style layers'),
35+
36+
// Additional metadata fields
37+
protected: z.boolean().optional().describe('Whether style is protected'),
38+
draft: z.boolean().optional().describe('Whether style is a draft')
39+
});
4240

4341
// API returns an array of styles
4442
const StylesArraySchema = z.array(StyleMetadataSchema);

src/tools/list-styles-tool/ListStylesTool.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,15 +86,15 @@ export class ListStylesTool extends MapboxApiBasedTool<
8686
}
8787
this.log('info', `ListStylesTool: Successfully listed styles`);
8888

89+
const wrappedData = { styles: parseResult.data };
8990
return {
9091
content: [
9192
{
9293
type: 'text' as const,
93-
text: JSON.stringify(parseResult.data, null, 2)
94+
text: JSON.stringify(wrappedData, null, 2)
9495
}
9596
],
96-
// Wrap the array in an object for structuredContent
97-
structuredContent: { styles: parseResult.data },
97+
structuredContent: wrappedData,
9898
isError: false
9999
};
100100
}

src/tools/list-tokens-tool/ListTokensTool.output.schema.ts

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,33 @@
44
import { z } from 'zod';
55

66
export const TokenObjectSchema = z.object({
7-
id: z.string().describe('Token ID'),
8-
scopes: z.array(z.string()).describe('Array of scopes assigned to the token'),
9-
token: z.string().describe('The actual token string'),
7+
id: z.string().describe("The token's unique identifier"),
8+
usage: z
9+
.enum(['pk', 'sk', 'tk'])
10+
.describe('Token usage type: pk (public), sk (secret), or tk (temporary)'),
11+
client: z.string().describe('The client for the token'),
12+
default: z.boolean().describe('Whether this is the default token'),
13+
scopes: z.array(z.string()).describe('Array of scopes granted to the token'),
14+
note: z
15+
.string()
16+
.nullable()
17+
.describe('Human-readable description of the token'),
1018
created: z.string().describe('ISO 8601 creation timestamp'),
1119
modified: z.string().describe('ISO 8601 last modified timestamp'),
12-
usage: z.string().describe('Token usage type, e.g. pk or sk'),
13-
default: z.boolean().describe('Whether this is the default token'),
14-
note: z.string().optional().describe('Optional note or description'),
15-
allowedUrls: z.array(z.string()).optional().describe('Array of allowed URLs'),
16-
expires: z.string().optional().describe('Expiration time in ISO 8601 format')
20+
allowedUrls: z
21+
.array(z.string())
22+
.optional()
23+
.describe('URLs that the token is restricted to'),
24+
token: z
25+
.string()
26+
.optional()
27+
.describe(
28+
'The actual access token string (omitted for secret tokens in list responses)'
29+
),
30+
expires: z
31+
.string()
32+
.optional()
33+
.describe('Expiration time in ISO 8601 format (temporary tokens only)')
1734
});
1835

1936
export const ListTokensOutputSchema = z.object({

src/tools/retrieve-style-tool/RetrieveStyleTool.output.schema.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,12 @@ export const MapboxStyleOutputSchema = BaseStylePropertiesSchema.extend({
2323
visibility: z
2424
.enum(['public', 'private'])
2525
.describe('Style visibility setting'),
26+
protected: z
27+
.boolean()
28+
.optional()
29+
.describe('Whether style is protected from modifications'),
2630
draft: z.boolean().optional().describe('Whether this is a draft version')
27-
}).passthrough();
31+
});
2832

2933
// Type exports
3034
export type MapboxStyleOutput = z.infer<typeof MapboxStyleOutputSchema>;

src/tools/tilequery-tool/TilequeryTool.output.schema.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ const CoordinatesSchema = z.tuple([z.number(), z.number()]);
99
// Vector Tileset Feature Schema
1010
const VectorTilequeryFeatureSchema = z.object({
1111
type: z.literal('Feature'),
12-
id: z.union([z.string(), z.number()]),
12+
id: z.string().describe('Feature identifier'),
1313
geometry: z.object({
1414
type: z.literal('Point'),
1515
coordinates: CoordinatesSchema
@@ -30,7 +30,7 @@ const VectorTilequeryFeatureSchema = z.object({
3030
.describe('The vector tile layer of the feature result')
3131
})
3232
})
33-
.passthrough() // Allow additional properties from the original feature
33+
.and(z.record(z.any())) // Allow additional properties from the original feature
3434
});
3535

3636
// Rasterarray Tileset Feature Schema
@@ -50,7 +50,11 @@ const RasterarrayTilequeryFeatureSchema = z.object({
5050
.describe('The maxzoom level at which the point value was extracted'),
5151
units: z.string().describe('The unit of measurement for the point value')
5252
}),
53-
val: z.number().describe('Point value at the requested location')
53+
val: z
54+
.union([z.number(), z.array(z.number())])
55+
.describe(
56+
'Point value at the requested location (number for single-band, array for multi-dimensional data)'
57+
)
5458
})
5559
});
5660

test/tools/create-token-tool/CreateTokenTool.test.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ describe('CreateTokenTool', () => {
151151
created: '2024-01-01T00:00:00.000Z',
152152
modified: '2024-01-01T00:00:00.000Z',
153153
usage: 'pk',
154+
client: 'api',
154155
default: false
155156
};
156157

@@ -206,6 +207,7 @@ describe('CreateTokenTool', () => {
206207
modified: '2024-01-01T00:00:00.000Z',
207208
allowedUrls: ['https://example.com', 'https://app.example.com'],
208209
usage: 'pk',
210+
client: 'api',
209211
default: false
210212
};
211213

@@ -246,6 +248,7 @@ describe('CreateTokenTool', () => {
246248
modified: '2024-01-01T00:00:00.000Z',
247249
expires: expiresAt,
248250
usage: 'pk',
251+
client: 'api',
249252
default: false
250253
};
251254

@@ -328,7 +331,10 @@ describe('CreateTokenTool', () => {
328331
id: 'cktest',
329332
scopes: ['styles:read'],
330333
created: '2024-01-01T00:00:00.000Z',
331-
modified: '2024-01-01T00:00:00.000Z'
334+
modified: '2024-01-01T00:00:00.000Z',
335+
usage: 'pk',
336+
client: 'api',
337+
default: false
332338
};
333339

334340
const { mockHttpRequest, httpRequest } = setupHttpRequest({

test/tools/list-styles-tool/ListStylesTool.test.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,8 @@ describe('ListStylesTool', () => {
199199
const content = result.content[0];
200200
if (content.type === 'text') {
201201
const parsedResponse = JSON.parse(content.text);
202-
expect(parsedResponse).toEqual(mockStyles);
202+
expect(parsedResponse).toHaveProperty('styles');
203+
expect(parsedResponse.styles).toEqual(mockStyles);
203204
}
204205

205206
// Verify structuredContent has the expected shape
@@ -250,8 +251,9 @@ describe('ListStylesTool', () => {
250251
const content = result.content[0];
251252
if (content.type === 'text') {
252253
const parsedResponse = JSON.parse(content.text);
253-
expect(parsedResponse).toHaveLength(1);
254-
expect(parsedResponse[0]).toMatchObject({
254+
expect(parsedResponse).toHaveProperty('styles');
255+
expect(parsedResponse.styles).toHaveLength(1);
256+
expect(parsedResponse.styles[0]).toMatchObject({
255257
id: 'ck9tnguii0ipm1ipf54wqhhwm',
256258
name: 'Yahoo! Japan Streets',
257259
owner: 'svc-okta-mapbox-staff-access',
@@ -260,7 +262,7 @@ describe('ListStylesTool', () => {
260262
protected: false
261263
});
262264
// Verify layers field is not required
263-
expect(parsedResponse[0].layers).toBeUndefined();
265+
expect(parsedResponse.styles[0].layers).toBeUndefined();
264266
}
265267

266268
assertHeadersSent(mockHttpRequest);

0 commit comments

Comments
 (0)