Skip to content

Commit b150fe3

Browse files
authored
Merge pull request #9 from mapbox/tilequery
Tilequery tool
2 parents d023bd6 + e88a4c1 commit b150fe3

File tree

5 files changed

+215
-1
lines changed

5 files changed

+215
-1
lines changed

src/tools/__snapshots__/tool-naming-convention.test.ts.snap

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,11 @@ exports[`Tool Naming Convention should maintain consistent tool list (snapshot t
6262
"description": "Generate a comparison URL for comparing two Mapbox styles side-by-side",
6363
"toolName": "style_comparison_tool",
6464
},
65+
{
66+
"className": "TilequeryTool",
67+
"description": "Query vector and raster data from Mapbox tilesets at geographic coordinates",
68+
"toolName": "tilequery_tool",
69+
},
6570
{
6671
"className": "UpdateStyleTool",
6772
"description": "Update an existing Mapbox style",
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { z } from 'zod';
2+
3+
export const TilequerySchema = z.object({
4+
tilesetId: z
5+
.string()
6+
.optional()
7+
.default('mapbox.mapbox-streets-v8')
8+
.describe('Tileset ID to query (default: mapbox.mapbox-streets-v8)'),
9+
longitude: z
10+
.number()
11+
.min(-180)
12+
.max(180)
13+
.describe('Longitude coordinate to query'),
14+
latitude: z
15+
.number()
16+
.min(-90)
17+
.max(90)
18+
.describe('Latitude coordinate to query'),
19+
radius: z
20+
.number()
21+
.min(0)
22+
.optional()
23+
.default(0)
24+
.describe('Radius in meters to search for features (default: 0)'),
25+
limit: z
26+
.number()
27+
.min(1)
28+
.max(50)
29+
.optional()
30+
.default(5)
31+
.describe('Number of features to return (1-50, default: 5)'),
32+
dedupe: z
33+
.boolean()
34+
.optional()
35+
.default(true)
36+
.describe('Whether to deduplicate identical features (default: true)'),
37+
geometry: z
38+
.enum(['polygon', 'linestring', 'point'])
39+
.optional()
40+
.describe('Filter results by geometry type'),
41+
layers: z
42+
.array(z.string())
43+
.optional()
44+
.describe('Specific layer names to query from the tileset'),
45+
bands: z
46+
.array(z.string())
47+
.optional()
48+
.describe('Specific band names to query (for rasterarray tilesets)')
49+
});
50+
51+
export type TilequeryInput = z.infer<typeof TilequerySchema>;
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { TilequeryTool } from './TilequeryTool.js';
2+
import { TilequeryInput } from './TilequeryTool.schema.js';
3+
4+
describe('TilequeryTool', () => {
5+
let tool: TilequeryTool;
6+
7+
beforeEach(() => {
8+
tool = new TilequeryTool();
9+
});
10+
11+
describe('constructor', () => {
12+
it('should initialize with correct name and description', () => {
13+
expect(tool.name).toBe('tilequery_tool');
14+
expect(tool.description).toBe(
15+
'Query vector and raster data from Mapbox tilesets at geographic coordinates'
16+
);
17+
});
18+
});
19+
20+
describe('schema validation', () => {
21+
it('should validate minimal valid input', () => {
22+
const input = {
23+
longitude: -122.4194,
24+
latitude: 37.7749
25+
};
26+
27+
const result = tool.inputSchema.safeParse(input);
28+
expect(result.success).toBe(true);
29+
if (result.success) {
30+
expect(result.data.tilesetId).toBe('mapbox.mapbox-streets-v8');
31+
expect(result.data.radius).toBe(0);
32+
expect(result.data.limit).toBe(5);
33+
expect(result.data.dedupe).toBe(true);
34+
}
35+
});
36+
37+
it('should validate complete input with all optional parameters', () => {
38+
const input: TilequeryInput = {
39+
tilesetId: 'custom.tileset',
40+
longitude: -122.4194,
41+
latitude: 37.7749,
42+
radius: 100,
43+
limit: 10,
44+
dedupe: false,
45+
geometry: 'polygon',
46+
layers: ['buildings', 'roads'],
47+
bands: ['band1', 'band2']
48+
};
49+
50+
const result = tool.inputSchema.safeParse(input);
51+
expect(result.success).toBe(true);
52+
});
53+
54+
it('should reject invalid longitude', () => {
55+
const input = {
56+
longitude: 181, // Invalid: > 180
57+
latitude: 37.7749
58+
};
59+
60+
const result = tool.inputSchema.safeParse(input);
61+
expect(result.success).toBe(false);
62+
});
63+
64+
it('should reject invalid latitude', () => {
65+
const input = {
66+
longitude: -122.4194,
67+
latitude: 91 // Invalid: > 90
68+
};
69+
70+
const result = tool.inputSchema.safeParse(input);
71+
expect(result.success).toBe(false);
72+
});
73+
74+
it('should reject limit outside valid range', () => {
75+
const input = {
76+
longitude: -122.4194,
77+
latitude: 37.7749,
78+
limit: 51 // Invalid: > 50
79+
};
80+
81+
const result = tool.inputSchema.safeParse(input);
82+
expect(result.success).toBe(false);
83+
});
84+
85+
it('should reject invalid geometry type', () => {
86+
const input = {
87+
longitude: -122.4194,
88+
latitude: 37.7749,
89+
geometry: 'invalid' as 'polygon'
90+
};
91+
92+
const result = tool.inputSchema.safeParse(input);
93+
expect(result.success).toBe(false);
94+
});
95+
});
96+
});
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { MapboxApiBasedTool } from '../MapboxApiBasedTool.js';
2+
import { TilequerySchema, TilequeryInput } from './TilequeryTool.schema.js';
3+
4+
export class TilequeryTool extends MapboxApiBasedTool<typeof TilequerySchema> {
5+
name = 'tilequery_tool';
6+
description =
7+
'Query vector and raster data from Mapbox tilesets at geographic coordinates';
8+
9+
constructor() {
10+
super({ inputSchema: TilequerySchema });
11+
}
12+
13+
protected async execute(
14+
input: TilequeryInput,
15+
accessToken?: string
16+
): Promise<any> {
17+
const { tilesetId, longitude, latitude, ...queryParams } = input;
18+
const url = new URL(
19+
`${MapboxApiBasedTool.MAPBOX_API_ENDPOINT}v4/${tilesetId}/tilequery/${longitude},${latitude}.json`
20+
);
21+
22+
if (queryParams.radius !== undefined) {
23+
url.searchParams.set('radius', queryParams.radius.toString());
24+
}
25+
26+
if (queryParams.limit !== undefined) {
27+
url.searchParams.set('limit', queryParams.limit.toString());
28+
}
29+
30+
if (queryParams.dedupe !== undefined) {
31+
url.searchParams.set('dedupe', queryParams.dedupe.toString());
32+
}
33+
34+
if (queryParams.geometry) {
35+
url.searchParams.set('geometry', queryParams.geometry);
36+
}
37+
38+
if (queryParams.layers && queryParams.layers.length > 0) {
39+
url.searchParams.set('layers', queryParams.layers.join(','));
40+
}
41+
42+
if (queryParams.bands && queryParams.bands.length > 0) {
43+
url.searchParams.set('bands', queryParams.bands.join(','));
44+
}
45+
46+
url.searchParams.set('access_token', accessToken || '');
47+
48+
const response = await fetch(url.toString());
49+
50+
if (!response.ok) {
51+
const errorText = await response.text();
52+
throw new Error(
53+
`Tilequery request failed: ${response.status} ${response.statusText}. ${errorText}`
54+
);
55+
}
56+
57+
const data = await response.json();
58+
return data;
59+
}
60+
}

src/tools/toolRegistry.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { ListTokensTool } from './list-tokens-tool/ListTokensTool.js';
1010
import { PreviewStyleTool } from './preview-style-tool/PreviewStyleTool.js';
1111
import { RetrieveStyleTool } from './retrieve-style-tool/RetrieveStyleTool.js';
1212
import { StyleComparisonTool } from './style-comparison-tool/StyleComparisonTool.js';
13+
import { TilequeryTool } from './tilequery-tool/TilequeryTool.js';
1314
import { UpdateStyleTool } from './update-style-tool/UpdateStyleTool.js';
1415

1516
// Central registry of all tools
@@ -26,7 +27,8 @@ export const ALL_TOOLS = [
2627
new BoundingBoxTool(),
2728
new CountryBoundingBoxTool(),
2829
new CoordinateConversionTool(),
29-
new StyleComparisonTool()
30+
new StyleComparisonTool(),
31+
new TilequeryTool()
3032
] as const;
3133

3234
export type ToolInstance = (typeof ALL_TOOLS)[number];

0 commit comments

Comments
 (0)