Skip to content
Merged
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
5 changes: 5 additions & 0 deletions src/tools/__snapshots__/tool-naming-convention.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ exports[`Tool Naming Convention should maintain consistent tool list (snapshot t
"description": "Generate a comparison URL for comparing two Mapbox styles side-by-side",
"toolName": "style_comparison_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",
Expand Down
51 changes: 51 additions & 0 deletions src/tools/tilequery-tool/TilequeryTool.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { z } from 'zod';

export const TilequerySchema = z.object({
tilesetId: z
.string()
.optional()
.default('mapbox.mapbox-streets-v8')
.describe('Tileset ID to query (default: mapbox.mapbox-streets-v8)'),
longitude: z
.number()
.min(-180)
.max(180)
.describe('Longitude coordinate to query'),
latitude: z
.number()
.min(-90)
.max(90)
.describe('Latitude coordinate to query'),
radius: z
.number()
.min(0)
.optional()
.default(0)
.describe('Radius in meters to search for features (default: 0)'),
limit: z
.number()
.min(1)
.max(50)
.optional()
.default(5)
.describe('Number of features to return (1-50, default: 5)'),
dedupe: z
.boolean()
.optional()
.default(true)
.describe('Whether to deduplicate identical features (default: true)'),
geometry: z
.enum(['polygon', 'linestring', 'point'])
.optional()
.describe('Filter results by geometry type'),
layers: z
.array(z.string())
.optional()
.describe('Specific layer names to query from the tileset'),
bands: z
.array(z.string())
.optional()
.describe('Specific band names to query (for rasterarray tilesets)')
});

export type TilequeryInput = z.infer<typeof TilequerySchema>;
96 changes: 96 additions & 0 deletions src/tools/tilequery-tool/TilequeryTool.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { TilequeryTool } from './TilequeryTool.js';
import { TilequeryInput } from './TilequeryTool.schema.js';

describe('TilequeryTool', () => {
let tool: TilequeryTool;

beforeEach(() => {
tool = new TilequeryTool();
});

describe('constructor', () => {
it('should initialize with correct name and description', () => {
expect(tool.name).toBe('tilequery_tool');
expect(tool.description).toBe(
'Query vector and raster data from Mapbox tilesets at geographic coordinates'
);
});
});

describe('schema validation', () => {
it('should validate minimal valid input', () => {
const input = {
longitude: -122.4194,
latitude: 37.7749
};

const result = tool.inputSchema.safeParse(input);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.tilesetId).toBe('mapbox.mapbox-streets-v8');
expect(result.data.radius).toBe(0);
expect(result.data.limit).toBe(5);
expect(result.data.dedupe).toBe(true);
}
});

it('should validate complete input with all optional parameters', () => {
const input: TilequeryInput = {
tilesetId: 'custom.tileset',
longitude: -122.4194,
latitude: 37.7749,
radius: 100,
limit: 10,
dedupe: false,
geometry: 'polygon',
layers: ['buildings', 'roads'],
bands: ['band1', 'band2']
};

const result = tool.inputSchema.safeParse(input);
expect(result.success).toBe(true);
});

it('should reject invalid longitude', () => {
const input = {
longitude: 181, // Invalid: > 180
latitude: 37.7749
};

const result = tool.inputSchema.safeParse(input);
expect(result.success).toBe(false);
});

it('should reject invalid latitude', () => {
const input = {
longitude: -122.4194,
latitude: 91 // Invalid: > 90
};

const result = tool.inputSchema.safeParse(input);
expect(result.success).toBe(false);
});

it('should reject limit outside valid range', () => {
const input = {
longitude: -122.4194,
latitude: 37.7749,
limit: 51 // Invalid: > 50
};

const result = tool.inputSchema.safeParse(input);
expect(result.success).toBe(false);
});

it('should reject invalid geometry type', () => {
const input = {
longitude: -122.4194,
latitude: 37.7749,
geometry: 'invalid' as 'polygon'
};

const result = tool.inputSchema.safeParse(input);
expect(result.success).toBe(false);
});
});
});
60 changes: 60 additions & 0 deletions src/tools/tilequery-tool/TilequeryTool.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { MapboxApiBasedTool } from '../MapboxApiBasedTool.js';
import { TilequerySchema, TilequeryInput } from './TilequeryTool.schema.js';

export class TilequeryTool extends MapboxApiBasedTool<typeof TilequerySchema> {
name = 'tilequery_tool';
description =
'Query vector and raster data from Mapbox tilesets at geographic coordinates';

constructor() {
super({ inputSchema: TilequerySchema });
}

protected async execute(
input: TilequeryInput,
accessToken?: string
): Promise<any> {
const { tilesetId, longitude, latitude, ...queryParams } = input;
const url = new URL(
`${MapboxApiBasedTool.MAPBOX_API_ENDPOINT}v4/${tilesetId}/tilequery/${longitude},${latitude}.json`
);

if (queryParams.radius !== undefined) {
url.searchParams.set('radius', queryParams.radius.toString());
}

if (queryParams.limit !== undefined) {
url.searchParams.set('limit', queryParams.limit.toString());
}

if (queryParams.dedupe !== undefined) {
url.searchParams.set('dedupe', queryParams.dedupe.toString());
}

if (queryParams.geometry) {
url.searchParams.set('geometry', queryParams.geometry);
}

if (queryParams.layers && queryParams.layers.length > 0) {
url.searchParams.set('layers', queryParams.layers.join(','));
}

if (queryParams.bands && queryParams.bands.length > 0) {
url.searchParams.set('bands', queryParams.bands.join(','));
}

url.searchParams.set('access_token', accessToken || '');

const response = await fetch(url.toString());

if (!response.ok) {
const errorText = await response.text();
throw new Error(
`Tilequery request failed: ${response.status} ${response.statusText}. ${errorText}`
);
}

const data = await response.json();
return data;
}
}
4 changes: 3 additions & 1 deletion src/tools/toolRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { ListTokensTool } from './list-tokens-tool/ListTokensTool.js';
import { PreviewStyleTool } from './preview-style-tool/PreviewStyleTool.js';
import { RetrieveStyleTool } from './retrieve-style-tool/RetrieveStyleTool.js';
import { StyleComparisonTool } from './style-comparison-tool/StyleComparisonTool.js';
import { TilequeryTool } from './tilequery-tool/TilequeryTool.js';
import { UpdateStyleTool } from './update-style-tool/UpdateStyleTool.js';

// Central registry of all tools
Expand All @@ -26,7 +27,8 @@ export const ALL_TOOLS = [
new BoundingBoxTool(),
new CountryBoundingBoxTool(),
new CoordinateConversionTool(),
new StyleComparisonTool()
new StyleComparisonTool(),
new TilequeryTool()
] as const;

export type ToolInstance = (typeof ALL_TOOLS)[number];
Expand Down
Loading