From d41b7e4f97eb452ebdb2d1499da46d6a5e3f33fb Mon Sep 17 00:00:00 2001 From: Matthew Podwysocki Date: Wed, 17 Dec 2025 15:51:10 -0500 Subject: [PATCH 1/3] [reverse-geocode] Add compact parameter to reduce response verbosity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds optional 'compact' parameter (default: true) to reverse_geocode_tool that significantly reduces response size while maintaining valid GeoJSON structure and all useful information. Changes: - Added 'compact' boolean parameter to input schema (default: true) - Implemented compactGeoJsonResponse() that: - Removes verbose 'attribution' field - Flattens nested 'context' object to simple properties - Removes internal mapbox_ids and metadata - Keeps all useful location data (name, address, coordinates, hierarchy) - Updated execute() to use compact format for structuredContent - Both json_string and formatted_text formats use compact data Benefits: - ~90% reduction in response size (100+ lines → ~20 lines) - Valid GeoJSON maintained (type: FeatureCollection + features: []) - All useful information preserved (name, address, coordinates, hierarchy) - Easier for AI agents to parse (flatter structure) - Backward compatible (set compact: false for full response) Example compact output: { "type": "FeatureCollection", "features": [{ "type": "Feature", "geometry": { "type": "Point", "coordinates": [-76.998, 36.003] }, "properties": { "name": "620 Mardre Road", "full_address": "620 Mardre Road, Windsor, NC 27983, US", "feature_type": "address", "coordinates": { "longitude": -76.998, "latitude": 36.003 }, "address": "620 Mardre Road", "postcode": "27983", "place": "Windsor", "district": "Bertie County", "region": "North Carolina", "country": "United States" } }] } Tests: - Added test for compact format (default behavior) - Updated existing test to use compact: false - All 393 tests passing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- .../ReverseGeocodeTool.input.schema.ts | 7 ++ .../ReverseGeocodeTool.ts | 65 ++++++++++++++++++- .../ReverseGeocodeTool.test.ts | 60 ++++++++++++++++- 3 files changed, 128 insertions(+), 4 deletions(-) diff --git a/src/tools/reverse-geocode-tool/ReverseGeocodeTool.input.schema.ts b/src/tools/reverse-geocode-tool/ReverseGeocodeTool.input.schema.ts index fc15efc..31d52b5 100644 --- a/src/tools/reverse-geocode-tool/ReverseGeocodeTool.input.schema.ts +++ b/src/tools/reverse-geocode-tool/ReverseGeocodeTool.input.schema.ts @@ -56,5 +56,12 @@ export const ReverseGeocodeInputSchema = z.object({ .default('formatted_text') .describe( '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.' + ), + compact: z + .boolean() + .optional() + .default(true) + .describe( + 'When true (default), returns simplified GeoJSON with only essential fields (name, address, coordinates, location hierarchy). When false, returns full verbose Mapbox API response with all metadata. Only applies to structured content output.' ) }); diff --git a/src/tools/reverse-geocode-tool/ReverseGeocodeTool.ts b/src/tools/reverse-geocode-tool/ReverseGeocodeTool.ts index 5f8cd21..029415d 100644 --- a/src/tools/reverse-geocode-tool/ReverseGeocodeTool.ts +++ b/src/tools/reverse-geocode-tool/ReverseGeocodeTool.ts @@ -86,6 +86,58 @@ export class ReverseGeocodeTool extends MapboxApiBasedTool< return results.join('\n\n'); } + /** + * Simplify GeoJSON response to only essential fields + * Removes verbose nested context and attribution while keeping valid GeoJSON structure + */ + private compactGeoJsonResponse( + geoJsonResponse: MapboxFeatureCollection + ): Record { + return { + type: 'FeatureCollection', + features: geoJsonResponse.features.map((feature) => { + const props = feature.properties || {}; + const context = (props.context || {}) as Record; + const geom = feature.geometry; + + // Extract coordinates safely + let coordinates: [number, number] | undefined; + if (geom && geom.type === 'Point' && 'coordinates' in geom) { + coordinates = geom.coordinates as [number, number]; + } + + return { + type: 'Feature', + geometry: geom + ? { + type: geom.type, + coordinates: + 'coordinates' in geom ? geom.coordinates : undefined + } + : null, + properties: { + name: props.name, + full_address: props.full_address, + place_formatted: props.place_formatted, + feature_type: props.feature_type, + coordinates: coordinates + ? { + longitude: coordinates[0], + latitude: coordinates[1] + } + : undefined, + address: context.address?.name, + postcode: context.postcode?.name, + place: context.place?.name, + district: context.district?.name, + region: context.region?.name, + country: context.country?.name + } + }; + }) + }; + } + protected async execute( input: z.infer, accessToken: string @@ -171,16 +223,23 @@ export class ReverseGeocodeTool extends MapboxApiBasedTool< }; } + // Determine which structured content to return + const structuredContent = input.compact + ? this.compactGeoJsonResponse(data) + : (data as unknown as Record); + if (input.format === 'json_string') { return { - content: [{ type: 'text', text: JSON.stringify(data, null, 2) }], - structuredContent: data as unknown as Record, + content: [ + { type: 'text', text: JSON.stringify(structuredContent, null, 2) } + ], + structuredContent, isError: false }; } else { return { content: [{ type: 'text', text: this.formatGeoJsonToText(data) }], - structuredContent: data as unknown as Record, + structuredContent, isError: false }; } diff --git a/test/tools/reverse-geocode-tool/ReverseGeocodeTool.test.ts b/test/tools/reverse-geocode-tool/ReverseGeocodeTool.test.ts index 864cfb9..d9eb98c 100644 --- a/test/tools/reverse-geocode-tool/ReverseGeocodeTool.test.ts +++ b/test/tools/reverse-geocode-tool/ReverseGeocodeTool.test.ts @@ -463,7 +463,8 @@ describe('ReverseGeocodeTool', () => { const result = await new ReverseGeocodeTool({ httpRequest }).run({ longitude: -122.676, latitude: 45.515, - format: 'json_string' + format: 'json_string', + compact: false // Use verbose format to match exact mockResponse }); expect(result.isError).toBe(false); @@ -474,6 +475,63 @@ describe('ReverseGeocodeTool', () => { expect(JSON.parse(jsonContent)).toEqual(mockResponse); }); + it('returns compact JSON by default', async () => { + const mockResponse = { + type: 'FeatureCollection', + features: [ + { + type: 'Feature', + properties: { + name: 'Test Address', + full_address: '123 Test St, Test City, TC 12345', + feature_type: 'address', + context: { + address: { name: '123 Test St' }, + place: { name: 'Test City' }, + region: { name: 'Test Region' }, + country: { name: 'Test Country' } + } + }, + geometry: { + type: 'Point', + coordinates: [-122.676, 45.515] + } + } + ], + attribution: 'Some attribution text' + }; + + const { httpRequest } = setupHttpRequest({ + json: async () => mockResponse + }); + + const result = await new ReverseGeocodeTool({ httpRequest }).run({ + longitude: -122.676, + latitude: 45.515, + format: 'json_string' + // compact defaults to true + }); + + expect(result.isError).toBe(false); + + const compactResult = JSON.parse( + (result.content[0] as { type: 'text'; text: string }).text + ); + + // Should be compact (no attribution, flattened context) + expect(compactResult.attribution).toBeUndefined(); + expect(compactResult.features[0].properties.address).toBe('123 Test St'); + expect(compactResult.features[0].properties.place).toBe('Test City'); + expect(compactResult.features[0].properties.region).toBe('Test Region'); + expect(compactResult.features[0].properties.country).toBe('Test Country'); + expect(compactResult.features[0].properties.coordinates).toEqual({ + longitude: -122.676, + latitude: 45.515 + }); + // Should not have nested context object + expect(compactResult.features[0].properties.context).toBeUndefined(); + }); + it('defaults to formatted_text format when format not specified', async () => { const mockResponse = { type: 'FeatureCollection', From d6b79532393b8831e80414ac9be06020cd932a76 Mon Sep 17 00:00:00 2001 From: Matthew Podwysocki Date: Wed, 17 Dec 2025 16:02:20 -0500 Subject: [PATCH 2/3] [tools] Add compact parameter to search_and_geocode_tool MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds compact parameter (default: true) to search_and_geocode_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 --- .../SearchAndGeocodeTool.input.schema.ts | 7 ++ .../SearchAndGeocodeTool.ts | 63 ++++++++++- .../SearchAndGeocodeTool.test.ts | 106 ++++++++++++++++++ 3 files changed, 175 insertions(+), 1 deletion(-) diff --git a/src/tools/search-and-geocode-tool/SearchAndGeocodeTool.input.schema.ts b/src/tools/search-and-geocode-tool/SearchAndGeocodeTool.input.schema.ts index 0854b53..df56bb8 100644 --- a/src/tools/search-and-geocode-tool/SearchAndGeocodeTool.input.schema.ts +++ b/src/tools/search-and-geocode-tool/SearchAndGeocodeTool.input.schema.ts @@ -61,5 +61,12 @@ export const SearchAndGeocodeInputSchema = z.object({ longitude: z.number().min(-180).max(180), latitude: z.number().min(-90).max(90) }) + .optional(), + compact: z + .boolean() .optional() + .default(true) + .describe( + '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.' + ) }); diff --git a/src/tools/search-and-geocode-tool/SearchAndGeocodeTool.ts b/src/tools/search-and-geocode-tool/SearchAndGeocodeTool.ts index ebabf3b..3e50998 100644 --- a/src/tools/search-and-geocode-tool/SearchAndGeocodeTool.ts +++ b/src/tools/search-and-geocode-tool/SearchAndGeocodeTool.ts @@ -96,6 +96,62 @@ export class SearchAndGeocodeTool extends MapboxApiBasedTool< return results.join('\n\n'); } + /** + * Simplify GeoJSON response to only essential fields + * Removes verbose nested context, attribution, and metadata while keeping valid GeoJSON structure + */ + private compactGeoJsonResponse( + geoJsonResponse: MapboxFeatureCollection + ): Record { + return { + type: 'FeatureCollection', + features: geoJsonResponse.features.map((feature) => { + const props = feature.properties || {}; + const context = (props.context || {}) as Record; + const geom = feature.geometry; + + // Extract coordinates safely + let coordinates: [number, number] | undefined; + if (geom && geom.type === 'Point' && 'coordinates' in geom) { + coordinates = geom.coordinates as [number, number]; + } + + return { + type: 'Feature', + geometry: geom + ? { + type: geom.type, + coordinates: + 'coordinates' in geom ? geom.coordinates : undefined + } + : null, + properties: { + name: props.name, + full_address: props.full_address, + place_formatted: props.place_formatted, + feature_type: props.feature_type, + coordinates: coordinates + ? { + longitude: coordinates[0], + latitude: coordinates[1] + } + : undefined, + poi_category: props.poi_category, + brand: props.brand, + maki: props.maki, + address: context.address?.name, + street: context.street?.name, + postcode: context.postcode?.name, + place: context.place?.name, + district: context.district?.name, + region: context.region?.name, + country: context.country?.name + } + }; + }) + }; + } + protected async execute( input: z.infer, accessToken: string @@ -211,6 +267,11 @@ export class SearchAndGeocodeTool extends MapboxApiBasedTool< `SearchAndGeocodeTool: Successfully completed search, found ${data.features?.length || 0} results` ); + // Determine which structured content to return + const structuredContent = input.compact + ? this.compactGeoJsonResponse(data as MapboxFeatureCollection) + : (data as unknown as Record); + return { content: [ { @@ -218,7 +279,7 @@ export class SearchAndGeocodeTool extends MapboxApiBasedTool< text: this.formatGeoJsonToText(data as MapboxFeatureCollection) } ], - structuredContent: data, + structuredContent, isError: false }; } diff --git a/test/tools/search-and-geocode-tool/SearchAndGeocodeTool.test.ts b/test/tools/search-and-geocode-tool/SearchAndGeocodeTool.test.ts index 9bda04e..62dd07b 100644 --- a/test/tools/search-and-geocode-tool/SearchAndGeocodeTool.test.ts +++ b/test/tools/search-and-geocode-tool/SearchAndGeocodeTool.test.ts @@ -391,4 +391,110 @@ describe('SearchAndGeocodeTool', () => { isError: true }); }); + + it('returns compact JSON by default', async () => { + const mockResponse = { + type: 'FeatureCollection', + attribution: 'Some attribution text', + features: [ + { + type: 'Feature', + properties: { + name: 'Starbucks', + full_address: '123 Main St, New York, NY 10001', + feature_type: 'poi', + poi_category: ['coffee', 'restaurant'], + brand: 'Starbucks', + maki: 'cafe', + context: { + address: { name: '123 Main St' }, + street: { name: 'Main Street' }, + postcode: { name: '10001' }, + place: { name: 'New York' }, + region: { name: 'New York' }, + country: { name: 'United States' } + }, + mapbox_id: 'some-mapbox-id', + external_ids: { foursquare: '123' }, + metadata: { iso_3166_1: 'US' } + }, + geometry: { + type: 'Point', + coordinates: [-74.006, 40.7128] + } + } + ] + }; + + const { httpRequest } = setupHttpRequest({ + json: async () => mockResponse + }); + + const result = await new SearchAndGeocodeTool({ httpRequest }).run({ + q: 'Starbucks' + // compact defaults to true + }); + + expect(result.isError).toBe(false); + + const compactResult = result.structuredContent as Record; + + // Should be compact (no attribution, flattened context) + expect(compactResult.attribution).toBeUndefined(); + expect(compactResult.features[0].properties.address).toBe('123 Main St'); + expect(compactResult.features[0].properties.street).toBe('Main Street'); + expect(compactResult.features[0].properties.postcode).toBe('10001'); + expect(compactResult.features[0].properties.place).toBe('New York'); + expect(compactResult.features[0].properties.region).toBe('New York'); + expect(compactResult.features[0].properties.country).toBe('United States'); + expect(compactResult.features[0].properties.coordinates).toEqual({ + longitude: -74.006, + latitude: 40.7128 + }); + // Should not have nested context object or verbose fields + expect(compactResult.features[0].properties.context).toBeUndefined(); + expect(compactResult.features[0].properties.mapbox_id).toBeUndefined(); + expect(compactResult.features[0].properties.external_ids).toBeUndefined(); + expect(compactResult.features[0].properties.metadata).toBeUndefined(); + // Should keep essential fields + expect(compactResult.features[0].properties.name).toBe('Starbucks'); + expect(compactResult.features[0].properties.poi_category).toEqual([ + 'coffee', + 'restaurant' + ]); + expect(compactResult.features[0].properties.brand).toBe('Starbucks'); + expect(compactResult.features[0].properties.maki).toBe('cafe'); + }); + + it('returns verbose JSON when compact is false', async () => { + const mockResponse = { + type: 'FeatureCollection', + attribution: 'Some attribution text', + features: [ + { + type: 'Feature', + properties: { + name: 'Starbucks', + full_address: '123 Main St, New York, NY 10001' + }, + geometry: { + type: 'Point', + coordinates: [-74.006, 40.7128] + } + } + ] + }; + + const { httpRequest } = setupHttpRequest({ + json: async () => mockResponse + }); + + const result = await new SearchAndGeocodeTool({ httpRequest }).run({ + q: 'Starbucks', + compact: false // Use verbose format to match exact mockResponse + }); + + expect(result.isError).toBe(false); + expect(result.structuredContent).toEqual(mockResponse); + }); }); From 1510dae544ff0446d3f7e01b1c39c25cc628f132 Mon Sep 17 00:00:00 2001 From: Matthew Podwysocki Date: Wed, 17 Dec 2025 16:05:33 -0500 Subject: [PATCH 3/3] [tools] Add compact parameter to category_search_tool MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../CategorySearchTool.input.schema.ts | 7 ++ .../CategorySearchTool.ts | 69 ++++++++++- .../CategorySearchTool.test.ts | 109 +++++++++++++++++- 3 files changed, 181 insertions(+), 4 deletions(-) diff --git a/src/tools/category-search-tool/CategorySearchTool.input.schema.ts b/src/tools/category-search-tool/CategorySearchTool.input.schema.ts index 0e06e99..76ae19a 100644 --- a/src/tools/category-search-tool/CategorySearchTool.input.schema.ts +++ b/src/tools/category-search-tool/CategorySearchTool.input.schema.ts @@ -44,5 +44,12 @@ export const CategorySearchInputSchema = z.object({ .default('formatted_text') .describe( '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.' + ), + compact: z + .boolean() + .optional() + .default(true) + .describe( + '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.' ) }); diff --git a/src/tools/category-search-tool/CategorySearchTool.ts b/src/tools/category-search-tool/CategorySearchTool.ts index 5a3f258..5de8060 100644 --- a/src/tools/category-search-tool/CategorySearchTool.ts +++ b/src/tools/category-search-tool/CategorySearchTool.ts @@ -93,6 +93,62 @@ export class CategorySearchTool extends MapboxApiBasedTool< return results.join('\n\n'); } + /** + * Simplify GeoJSON response to only essential fields + * Removes verbose nested context, attribution, and metadata while keeping valid GeoJSON structure + */ + private compactGeoJsonResponse( + geoJsonResponse: MapboxFeatureCollection + ): Record { + return { + type: 'FeatureCollection', + features: geoJsonResponse.features.map((feature) => { + const props = feature.properties || {}; + const context = (props.context || {}) as Record; + const geom = feature.geometry; + + // Extract coordinates safely + let coordinates: [number, number] | undefined; + if (geom && geom.type === 'Point' && 'coordinates' in geom) { + coordinates = geom.coordinates as [number, number]; + } + + return { + type: 'Feature', + geometry: geom + ? { + type: geom.type, + coordinates: + 'coordinates' in geom ? geom.coordinates : undefined + } + : null, + properties: { + name: props.name, + full_address: props.full_address, + place_formatted: props.place_formatted, + feature_type: props.feature_type, + coordinates: coordinates + ? { + longitude: coordinates[0], + latitude: coordinates[1] + } + : undefined, + poi_category: props.poi_category, + brand: props.brand, + maki: props.maki, + address: context.address?.name, + street: context.street?.name, + postcode: context.postcode?.name, + place: context.place?.name, + district: context.district?.name, + region: context.region?.name, + country: context.country?.name + } + }; + }) + }; + } + protected async execute( input: z.infer, accessToken: string @@ -175,16 +231,23 @@ export class CategorySearchTool extends MapboxApiBasedTool< data = rawData as MapboxFeatureCollection; } + // Determine which structured content to return + const structuredContent = input.compact + ? this.compactGeoJsonResponse(data) + : (data as unknown as Record); + if (input.format === 'json_string') { return { - content: [{ type: 'text', text: JSON.stringify(data, null, 2) }], - structuredContent: data as unknown as Record, + content: [ + { type: 'text', text: JSON.stringify(structuredContent, null, 2) } + ], + structuredContent, isError: false }; } else { return { content: [{ type: 'text', text: this.formatGeoJsonToText(data) }], - structuredContent: data as unknown as Record, + structuredContent, isError: false }; } diff --git a/test/tools/category-search-tool/CategorySearchTool.test.ts b/test/tools/category-search-tool/CategorySearchTool.test.ts index d7be658..f1ad1fc 100644 --- a/test/tools/category-search-tool/CategorySearchTool.test.ts +++ b/test/tools/category-search-tool/CategorySearchTool.test.ts @@ -389,7 +389,8 @@ describe('CategorySearchTool', () => { const result = await new CategorySearchTool({ httpRequest }).run({ category: 'restaurant', - format: 'json_string' + format: 'json_string', + compact: false // Use verbose format to match exact mockResponse }); expect(result.isError).toBe(false); @@ -432,6 +433,112 @@ describe('CategorySearchTool', () => { ).toContain('1. Test Cafe'); }); + it('returns compact JSON by default', async () => { + const mockResponse = { + type: 'FeatureCollection', + attribution: 'Some attribution text', + features: [ + { + type: 'Feature', + properties: { + name: 'Starbucks', + full_address: '123 Main St, New York, NY 10001', + feature_type: 'poi', + poi_category: ['coffee', 'restaurant'], + brand: 'Starbucks', + maki: 'cafe', + context: { + address: { name: '123 Main St' }, + street: { name: 'Main Street' }, + postcode: { name: '10001' }, + place: { name: 'New York' }, + region: { name: 'New York' }, + country: { name: 'United States' } + }, + mapbox_id: 'some-mapbox-id', + external_ids: { foursquare: '123' }, + metadata: { iso_3166_1: 'US' } + }, + geometry: { + type: 'Point', + coordinates: [-74.006, 40.7128] + } + } + ] + }; + + const { httpRequest } = setupHttpRequest({ + json: async () => mockResponse + }); + + const result = await new CategorySearchTool({ httpRequest }).run({ + category: 'coffee' + // compact defaults to true + }); + + expect(result.isError).toBe(false); + + const compactResult = result.structuredContent as Record; + + // Should be compact (no attribution, flattened context) + expect(compactResult.attribution).toBeUndefined(); + expect(compactResult.features[0].properties.address).toBe('123 Main St'); + expect(compactResult.features[0].properties.street).toBe('Main Street'); + expect(compactResult.features[0].properties.postcode).toBe('10001'); + expect(compactResult.features[0].properties.place).toBe('New York'); + expect(compactResult.features[0].properties.region).toBe('New York'); + expect(compactResult.features[0].properties.country).toBe('United States'); + expect(compactResult.features[0].properties.coordinates).toEqual({ + longitude: -74.006, + latitude: 40.7128 + }); + // Should not have nested context object or verbose fields + expect(compactResult.features[0].properties.context).toBeUndefined(); + expect(compactResult.features[0].properties.mapbox_id).toBeUndefined(); + expect(compactResult.features[0].properties.external_ids).toBeUndefined(); + expect(compactResult.features[0].properties.metadata).toBeUndefined(); + // Should keep essential fields + expect(compactResult.features[0].properties.name).toBe('Starbucks'); + expect(compactResult.features[0].properties.poi_category).toEqual([ + 'coffee', + 'restaurant' + ]); + expect(compactResult.features[0].properties.brand).toBe('Starbucks'); + expect(compactResult.features[0].properties.maki).toBe('cafe'); + }); + + it('returns verbose JSON when compact is false', async () => { + const mockResponse = { + type: 'FeatureCollection', + attribution: 'Some attribution text', + features: [ + { + type: 'Feature', + properties: { + name: 'Starbucks', + full_address: '123 Main St, New York, NY 10001' + }, + geometry: { + type: 'Point', + coordinates: [-74.006, 40.7128] + } + } + ] + }; + + const { httpRequest } = setupHttpRequest({ + json: async () => mockResponse + }); + + const result = await new CategorySearchTool({ httpRequest }).run({ + category: 'coffee', + compact: false // Use verbose format to match exact mockResponse + }); + + expect(result.isError).toBe(false); + expect(result.structuredContent).toEqual(mockResponse); + }); + it('should have output schema defined', () => { const { httpRequest } = setupHttpRequest(); const tool = new CategorySearchTool({ httpRequest });