Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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 @@ -57,6 +57,11 @@ exports[`Tool Naming Convention should maintain consistent tool list (snapshot t
"description": "Retrieve a specific Mapbox style by ID",
"toolName": "retrieve_style_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 @@ -9,6 +9,7 @@ import { ListStylesTool } from './list-styles-tool/ListStylesTool.js';
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 { TilequeryTool } from './tilequery-tool/TilequeryTool.js';
import { UpdateStyleTool } from './update-style-tool/UpdateStyleTool.js';

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

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