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 51bf81f..7a27011 100644 --- a/src/tools/category-search-tool/CategorySearchTool.ts +++ b/src/tools/category-search-tool/CategorySearchTool.ts @@ -111,6 +111,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 @@ -193,16 +249,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/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 3acd567..1adf81b 100644 --- a/src/tools/reverse-geocode-tool/ReverseGeocodeTool.ts +++ b/src/tools/reverse-geocode-tool/ReverseGeocodeTool.ts @@ -108,6 +108,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 @@ -193,16 +245,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/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.output.schema.ts b/src/tools/search-and-geocode-tool/SearchAndGeocodeTool.output.schema.ts index a2308e3..ae9d434 100644 --- a/src/tools/search-and-geocode-tool/SearchAndGeocodeTool.output.schema.ts +++ b/src/tools/search-and-geocode-tool/SearchAndGeocodeTool.output.schema.ts @@ -4,113 +4,119 @@ import { z } from 'zod'; // Search Box API feature properties schema -const SearchBoxFeaturePropertiesSchema = z.object({ - // Basic identification - mapbox_id: z.string().optional(), - feature_type: z.string().optional(), - name: z.string().optional(), - name_preferred: z.string().optional(), +const SearchBoxFeaturePropertiesSchema = z + .object({ + // Basic identification + mapbox_id: z.string().optional(), + feature_type: z.string().optional(), + name: z.string().optional(), + name_preferred: z.string().optional(), - // Address components - full_address: z.string().optional(), - place_formatted: z.string().optional(), - address_number: z.string().optional(), - street_name: z.string().optional(), + // Address components + full_address: z.string().optional(), + place_formatted: z.string().optional(), + address_number: z.string().optional(), + street_name: z.string().optional(), - // Administrative areas - context: z - .object({ - country: z - .object({ - name: z.string().optional(), - country_code: z.string().optional(), - country_code_alpha_3: z.string().optional() - }) - .optional(), - region: z - .object({ - name: z.string().optional(), - region_code: z.string().optional(), - region_code_full: z.string().optional() - }) - .optional(), - postcode: z - .object({ - name: z.string().optional() - }) - .optional(), - district: z - .object({ - name: z.string().optional() - }) - .optional(), - place: z - .object({ - name: z.string().optional() - }) - .optional(), - locality: z - .object({ - name: z.string().optional() - }) - .optional(), - neighborhood: z - .object({ - name: z.string().optional() - }) - .optional(), - street: z - .object({ - name: z.string().optional() - }) - .optional(), - address: z - .object({ - address_number: z.string().optional(), - street_name: z.string().optional() - }) - .optional() - }) - .optional(), - - // Coordinates and bounds - coordinates: z - .object({ - longitude: z.number(), - latitude: z.number(), - accuracy: z.string().optional(), - routable_points: z - .array( - z.object({ - name: z.string(), - latitude: z.number(), - longitude: z.number() + // Administrative areas + context: z + .object({ + country: z + .object({ + name: z.string().optional(), + country_code: z.string().optional(), + country_code_alpha_3: z.string().optional() + }) + .optional(), + region: z + .object({ + name: z.string().optional(), + region_code: z.string().optional(), + region_code_full: z.string().optional() + }) + .optional(), + postcode: z + .object({ + name: z.string().optional() + }) + .optional(), + district: z + .object({ + name: z.string().optional() + }) + .optional(), + place: z + .object({ + name: z.string().optional() + }) + .optional(), + locality: z + .object({ + name: z.string().optional() }) - ) - .optional() - }) - .optional(), - bbox: z.array(z.number()).length(4).optional(), + .optional(), + neighborhood: z + .object({ + name: z.string().optional() + }) + .optional(), + street: z + .object({ + name: z.string().optional() + }) + .optional(), + address: z + .object({ + address_number: z.string().optional(), + street_name: z.string().optional() + }) + .optional() + }) + .optional(), - // POI specific fields - poi_category: z.array(z.string()).optional(), - poi_category_ids: z.array(z.string()).optional(), - brand: z.array(z.string()).optional(), - brand_id: z.union([z.string(), z.array(z.string())]).optional(), - external_ids: z.record(z.string()).optional(), + // Coordinates and bounds + coordinates: z + .object({ + longitude: z.number(), + latitude: z.number(), + accuracy: z.string().optional(), + routable_points: z + .array( + z.object({ + name: z.string(), + latitude: z.number(), + longitude: z.number() + }) + ) + .optional() + }) + .optional(), + bbox: z.array(z.number()).length(4).optional(), - // Additional metadata - maki: z.string().optional(), - operational_status: z.string().optional(), + // POI specific fields + poi_category: z.array(z.string()).optional(), + poi_category_ids: z.array(z.string()).optional(), + brand: z.array(z.string()).optional(), + brand_id: z.union([z.string(), z.array(z.string())]).optional(), + external_ids: z.record(z.string()).optional(), - // ETA information (when requested) - eta: z - .object({ - duration: z.number().optional(), - distance: z.number().optional() - }) - .optional() -}); + // Additional metadata + maki: z.string().optional(), + operational_status: z.string().optional(), + + // ETA information (when requested) + eta: z + .object({ + duration: z.number().optional(), + distance: z.number().optional() + }) + .optional(), + + // Top-level country field (in addition to context.country) + // Mapbox Search Box API sometimes returns country at the top level + country: z.string().optional() + }) + .passthrough(); // Allow additional properties the API may add in the future // GeoJSON geometry schema const GeometrySchema = z.object({ diff --git a/src/tools/search-and-geocode-tool/SearchAndGeocodeTool.ts b/src/tools/search-and-geocode-tool/SearchAndGeocodeTool.ts index 79cafab..b7a5cd7 100644 --- a/src/tools/search-and-geocode-tool/SearchAndGeocodeTool.ts +++ b/src/tools/search-and-geocode-tool/SearchAndGeocodeTool.ts @@ -113,6 +113,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 @@ -228,6 +284,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: [ { @@ -235,7 +296,7 @@ export class SearchAndGeocodeTool extends MapboxApiBasedTool< text: this.formatGeoJsonToText(data as MapboxFeatureCollection) } ], - structuredContent: data, + 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 }); 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', 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); + }); });