Skip to content

Commit 1510dae

Browse files
[tools] Add compact parameter to category_search_tool
Adds compact parameter (default: true) to category_search_tool to reduce response size by ~90% while maintaining valid GeoJSON structure. - Removes attribution, external_ids, mapbox_id, metadata - Flattens nested context object into direct properties - Keeps essential fields: name, address, coordinates, poi_category, brand, maki - Maintains valid GeoJSON structure for use in geojson.io and other tools - Backward compatible with compact: false for full verbose response 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
1 parent d6b7953 commit 1510dae

File tree

3 files changed

+181
-4
lines changed

3 files changed

+181
-4
lines changed

src/tools/category-search-tool/CategorySearchTool.input.schema.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,5 +44,12 @@ export const CategorySearchInputSchema = z.object({
4444
.default('formatted_text')
4545
.describe(
4646
'Output format: "json_string" returns raw GeoJSON data as a JSON string that can be parsed; "formatted_text" returns human-readable text with place names, addresses, and coordinates. Both return as text content but json_string contains parseable JSON data while formatted_text is for display.'
47+
),
48+
compact: z
49+
.boolean()
50+
.optional()
51+
.default(true)
52+
.describe(
53+
'When true (default), returns simplified GeoJSON with only essential fields (name, address, coordinates, categories, brand). When false, returns full verbose Mapbox API response with all metadata. Only applies to structured content output.'
4754
)
4855
});

src/tools/category-search-tool/CategorySearchTool.ts

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,62 @@ export class CategorySearchTool extends MapboxApiBasedTool<
9393
return results.join('\n\n');
9494
}
9595

96+
/**
97+
* Simplify GeoJSON response to only essential fields
98+
* Removes verbose nested context, attribution, and metadata while keeping valid GeoJSON structure
99+
*/
100+
private compactGeoJsonResponse(
101+
geoJsonResponse: MapboxFeatureCollection
102+
): Record<string, unknown> {
103+
return {
104+
type: 'FeatureCollection',
105+
features: geoJsonResponse.features.map((feature) => {
106+
const props = feature.properties || {};
107+
const context = (props.context || {}) as Record<string, any>;
108+
const geom = feature.geometry;
109+
110+
// Extract coordinates safely
111+
let coordinates: [number, number] | undefined;
112+
if (geom && geom.type === 'Point' && 'coordinates' in geom) {
113+
coordinates = geom.coordinates as [number, number];
114+
}
115+
116+
return {
117+
type: 'Feature',
118+
geometry: geom
119+
? {
120+
type: geom.type,
121+
coordinates:
122+
'coordinates' in geom ? geom.coordinates : undefined
123+
}
124+
: null,
125+
properties: {
126+
name: props.name,
127+
full_address: props.full_address,
128+
place_formatted: props.place_formatted,
129+
feature_type: props.feature_type,
130+
coordinates: coordinates
131+
? {
132+
longitude: coordinates[0],
133+
latitude: coordinates[1]
134+
}
135+
: undefined,
136+
poi_category: props.poi_category,
137+
brand: props.brand,
138+
maki: props.maki,
139+
address: context.address?.name,
140+
street: context.street?.name,
141+
postcode: context.postcode?.name,
142+
place: context.place?.name,
143+
district: context.district?.name,
144+
region: context.region?.name,
145+
country: context.country?.name
146+
}
147+
};
148+
})
149+
};
150+
}
151+
96152
protected async execute(
97153
input: z.infer<typeof CategorySearchInputSchema>,
98154
accessToken: string
@@ -175,16 +231,23 @@ export class CategorySearchTool extends MapboxApiBasedTool<
175231
data = rawData as MapboxFeatureCollection;
176232
}
177233

234+
// Determine which structured content to return
235+
const structuredContent = input.compact
236+
? this.compactGeoJsonResponse(data)
237+
: (data as unknown as Record<string, unknown>);
238+
178239
if (input.format === 'json_string') {
179240
return {
180-
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
181-
structuredContent: data as unknown as Record<string, unknown>,
241+
content: [
242+
{ type: 'text', text: JSON.stringify(structuredContent, null, 2) }
243+
],
244+
structuredContent,
182245
isError: false
183246
};
184247
} else {
185248
return {
186249
content: [{ type: 'text', text: this.formatGeoJsonToText(data) }],
187-
structuredContent: data as unknown as Record<string, unknown>,
250+
structuredContent,
188251
isError: false
189252
};
190253
}

test/tools/category-search-tool/CategorySearchTool.test.ts

Lines changed: 108 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -389,7 +389,8 @@ describe('CategorySearchTool', () => {
389389

390390
const result = await new CategorySearchTool({ httpRequest }).run({
391391
category: 'restaurant',
392-
format: 'json_string'
392+
format: 'json_string',
393+
compact: false // Use verbose format to match exact mockResponse
393394
});
394395

395396
expect(result.isError).toBe(false);
@@ -432,6 +433,112 @@ describe('CategorySearchTool', () => {
432433
).toContain('1. Test Cafe');
433434
});
434435

436+
it('returns compact JSON by default', async () => {
437+
const mockResponse = {
438+
type: 'FeatureCollection',
439+
attribution: 'Some attribution text',
440+
features: [
441+
{
442+
type: 'Feature',
443+
properties: {
444+
name: 'Starbucks',
445+
full_address: '123 Main St, New York, NY 10001',
446+
feature_type: 'poi',
447+
poi_category: ['coffee', 'restaurant'],
448+
brand: 'Starbucks',
449+
maki: 'cafe',
450+
context: {
451+
address: { name: '123 Main St' },
452+
street: { name: 'Main Street' },
453+
postcode: { name: '10001' },
454+
place: { name: 'New York' },
455+
region: { name: 'New York' },
456+
country: { name: 'United States' }
457+
},
458+
mapbox_id: 'some-mapbox-id',
459+
external_ids: { foursquare: '123' },
460+
metadata: { iso_3166_1: 'US' }
461+
},
462+
geometry: {
463+
type: 'Point',
464+
coordinates: [-74.006, 40.7128]
465+
}
466+
}
467+
]
468+
};
469+
470+
const { httpRequest } = setupHttpRequest({
471+
json: async () => mockResponse
472+
});
473+
474+
const result = await new CategorySearchTool({ httpRequest }).run({
475+
category: 'coffee'
476+
// compact defaults to true
477+
});
478+
479+
expect(result.isError).toBe(false);
480+
481+
const compactResult = result.structuredContent as Record<string, any>;
482+
483+
// Should be compact (no attribution, flattened context)
484+
expect(compactResult.attribution).toBeUndefined();
485+
expect(compactResult.features[0].properties.address).toBe('123 Main St');
486+
expect(compactResult.features[0].properties.street).toBe('Main Street');
487+
expect(compactResult.features[0].properties.postcode).toBe('10001');
488+
expect(compactResult.features[0].properties.place).toBe('New York');
489+
expect(compactResult.features[0].properties.region).toBe('New York');
490+
expect(compactResult.features[0].properties.country).toBe('United States');
491+
expect(compactResult.features[0].properties.coordinates).toEqual({
492+
longitude: -74.006,
493+
latitude: 40.7128
494+
});
495+
// Should not have nested context object or verbose fields
496+
expect(compactResult.features[0].properties.context).toBeUndefined();
497+
expect(compactResult.features[0].properties.mapbox_id).toBeUndefined();
498+
expect(compactResult.features[0].properties.external_ids).toBeUndefined();
499+
expect(compactResult.features[0].properties.metadata).toBeUndefined();
500+
// Should keep essential fields
501+
expect(compactResult.features[0].properties.name).toBe('Starbucks');
502+
expect(compactResult.features[0].properties.poi_category).toEqual([
503+
'coffee',
504+
'restaurant'
505+
]);
506+
expect(compactResult.features[0].properties.brand).toBe('Starbucks');
507+
expect(compactResult.features[0].properties.maki).toBe('cafe');
508+
});
509+
510+
it('returns verbose JSON when compact is false', async () => {
511+
const mockResponse = {
512+
type: 'FeatureCollection',
513+
attribution: 'Some attribution text',
514+
features: [
515+
{
516+
type: 'Feature',
517+
properties: {
518+
name: 'Starbucks',
519+
full_address: '123 Main St, New York, NY 10001'
520+
},
521+
geometry: {
522+
type: 'Point',
523+
coordinates: [-74.006, 40.7128]
524+
}
525+
}
526+
]
527+
};
528+
529+
const { httpRequest } = setupHttpRequest({
530+
json: async () => mockResponse
531+
});
532+
533+
const result = await new CategorySearchTool({ httpRequest }).run({
534+
category: 'coffee',
535+
compact: false // Use verbose format to match exact mockResponse
536+
});
537+
538+
expect(result.isError).toBe(false);
539+
expect(result.structuredContent).toEqual(mockResponse);
540+
});
541+
435542
it('should have output schema defined', () => {
436543
const { httpRequest } = setupHttpRequest();
437544
const tool = new CategorySearchTool({ httpRequest });

0 commit comments

Comments
 (0)