Skip to content

Commit 5394031

Browse files
cablateclaude
andcommitted
feat: add maps_batch_geocode MCP tool — geocode up to 50 addresses in one call
- New tool: maps_batch_geocode with addresses[] param (max 50) - Parallel geocoding with per-address error handling - Returns { total, succeeded, failed, results[] } - Full test coverage: MCP tool call + exec mode + registration - All 9 files synced per Tool Change Checklist (15 → 16 tools) - 125 smoke test assertions, 0 failures Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 4b06c94 commit 5394031

7 files changed

Lines changed: 141 additions & 5 deletions

File tree

README.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,15 @@
66

77
Give your AI agent the ability to understand the physical world — geocode, route, search, and reason about locations.
88

9-
- **15 tools**12 atomic + 3 composite (explore-area, plan-route, compare-places)
9+
- **16 tools**13 atomic + 3 composite (explore-area, plan-route, compare-places)
1010
- **3 modes** — stdio, StreamableHTTP, standalone exec CLI
1111
- **Agent Skill** — built-in skill definition teaches AI how to chain geo tools ([`skills/google-maps/`](./skills/google-maps/))
1212

1313
### vs Google Grounding Lite
1414

1515
| | This project | [Grounding Lite](https://cloud.google.com/blog/products/ai-machine-learning/announcing-official-mcp-support-for-google-services) |
1616
|---|---|---|
17-
| Tools | **15** | 3 |
17+
| Tools | **16** | 3 |
1818
| Geocoding | Yes | No |
1919
| Step-by-step directions | Yes | No |
2020
| Elevation | Yes | No |
@@ -62,6 +62,7 @@ Special thanks to [@junyinnnn](https://github.com/junyinnnn) for helping add sup
6262
| `maps_weather` | Get current weather conditions or forecast — temperature, humidity, wind, UV, precipitation. |
6363
| `maps_air_quality` | Get air quality index, pollutant concentrations, and health recommendations by demographic group. |
6464
| `maps_static_map` | Generate a map image with markers, paths, or routes — returned inline for the user to see directly. |
65+
| `maps_batch_geocode` | Geocode up to 50 addresses in one call — returns coordinates for each. |
6566
| **Composite Tools** | |
6667
| `maps_explore_area` | Explore what's around a location — searches multiple place types and gets details in one call. |
6768
| `maps_plan_route` | Plan an optimized multi-stop route — geocodes, finds best order, returns directions. |
@@ -115,7 +116,7 @@ Then configure your MCP client:
115116
### Server Information
116117

117118
- **Transport**: stdio (`--stdio`) or Streamable HTTP (default)
118-
- **Tools**: 15 Google Maps tools (12 atomic + 3 composite)
119+
- **Tools**: 16 Google Maps tools (13 atomic + 3 composite)
119120

120121
### CLI Exec Mode (Agent Skill)
121122

@@ -126,7 +127,7 @@ npx @cablate/mcp-google-map exec geocode '{"address":"Tokyo Tower"}'
126127
npx @cablate/mcp-google-map exec search-places '{"query":"ramen in Tokyo"}'
127128
```
128129

129-
All 15 tools available: `geocode`, `reverse-geocode`, `search-nearby`, `search-places`, `place-details`, `directions`, `distance-matrix`, `elevation`, `timezone`, `weather`, `air-quality`, `static-map`, `explore-area`, `plan-route`, `compare-places`. See [`skills/google-maps/`](./skills/google-maps/) for the agent skill definition and full parameter docs.
130+
All 16 tools available: `geocode`, `reverse-geocode`, `search-nearby`, `search-places`, `place-details`, `directions`, `distance-matrix`, `elevation`, `timezone`, `weather`, `air-quality`, `static-map`, `batch-geocode-tool`, `explore-area`, `plan-route`, `compare-places`. See [`skills/google-maps/`](./skills/google-maps/) for the agent skill definition and full parameter docs.
130131

131132
### Batch Geocode
132133

@@ -234,6 +235,7 @@ src/
234235
│ ├── weather.ts # maps_weather tool
235236
│ ├── airQuality.ts # maps_air_quality tool
236237
│ ├── staticMap.ts # maps_static_map tool
238+
│ ├── batchGeocode.ts # maps_batch_geocode tool
237239
│ ├── exploreArea.ts # maps_explore_area (composite)
238240
│ ├── planRoute.ts # maps_plan_route (composite)
239241
│ └── comparePlaces.ts # maps_compare_places (composite)

skills/google-maps/SKILL.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ Without this Skill, the agent can only guess or refuse when asked "how do I get
3535

3636
## Tool Map
3737

38-
15 tools in five categories — pick by scenario:
38+
16 tools in five categories — pick by scenario:
3939

4040
### Place Discovery
4141
| Tool | When to use | Example |
@@ -45,6 +45,7 @@ Without this Skill, the agent can only guess or refuse when asked "how do I get
4545
| `search-nearby` | Know a location, find nearby places by type | "Coffee shops near my hotel" |
4646
| `search-places` | Natural language place search | "Best ramen in Tokyo" |
4747
| `place-details` | Have a place_id, need full info | "Opening hours and reviews for this restaurant?" |
48+
| `batch-geocode` | Geocode multiple addresses at once (max 50) | "Get coordinates for all these offices" |
4849

4950
### Routing & Distance
5051
| Tool | When to use | Example |

skills/google-maps/references/tools-api.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,33 @@ Response:
2626

2727
---
2828

29+
## batch-geocode
30+
31+
Geocode multiple addresses in one call (max 50).
32+
33+
```bash
34+
exec batch-geocode-tool '{"addresses": ["Tokyo Tower", "Eiffel Tower", "Statue of Liberty"]}'
35+
```
36+
37+
| Param | Type | Required | Description |
38+
|-------|------|----------|-------------|
39+
| addresses | string[] | yes | List of addresses or landmarks (max 50) |
40+
41+
Response:
42+
```json
43+
{
44+
"total": 3,
45+
"succeeded": 3,
46+
"failed": 0,
47+
"results": [
48+
{ "address": "Tokyo Tower", "success": true, "data": { "location": { "lat": 35.658, "lng": 139.745 }, "formatted_address": "..." } },
49+
...
50+
]
51+
}
52+
```
53+
54+
---
55+
2956
## reverse-geocode
3057

3158
Convert GPS coordinates to a street address.

src/cli.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ const EXEC_TOOLS = [
9292
"compare-places",
9393
"air-quality",
9494
"static-map",
95+
"batch-geocode-tool",
9596
] as const;
9697

9798
async function execTool(toolName: string, params: any, apiKey: string): Promise<any> {
@@ -183,6 +184,22 @@ async function execTool(toolName: string, params: any, apiKey: string): Promise<
183184
case "maps_static_map":
184185
return searcher.getStaticMap(params);
185186

187+
case "batch-geocode-tool":
188+
case "maps_batch_geocode": {
189+
const results = await Promise.all(
190+
(params.addresses as string[]).map(async (address: string) => {
191+
try {
192+
const result = await searcher.geocode(address);
193+
return { address, ...result };
194+
} catch (error: any) {
195+
return { address, success: false, error: error.message };
196+
}
197+
})
198+
);
199+
const succeeded = results.filter((r) => r.success).length;
200+
return { success: true, data: { total: params.addresses.length, succeeded, failed: params.addresses.length - succeeded, results } };
201+
}
202+
186203
default:
187204
throw new Error(`Unknown tool: ${toolName}. Available: ${EXEC_TOOLS.join(", ")}`);
188205
}

src/config.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { PlanRoute, PlanRouteParams } from "./tools/maps/planRoute.js";
1616
import { ComparePlaces, ComparePlacesParams } from "./tools/maps/comparePlaces.js";
1717
import { AirQuality, AirQualityParams } from "./tools/maps/airQuality.js";
1818
import { StaticMap, StaticMapParams } from "./tools/maps/staticMap.js";
19+
import { BatchGeocode, BatchGeocodeParams } from "./tools/maps/batchGeocode.js";
1920

2021
// All Google Maps tools are read-only API queries
2122
const MAPS_TOOL_ANNOTATIONS = {
@@ -141,6 +142,13 @@ const serverConfigs: ServerInstanceConfig[] = [
141142
annotations: MAPS_TOOL_ANNOTATIONS,
142143
action: (params: StaticMapParams) => StaticMap.ACTION(params),
143144
},
145+
{
146+
name: BatchGeocode.NAME,
147+
description: BatchGeocode.DESCRIPTION,
148+
schema: BatchGeocode.SCHEMA,
149+
annotations: MAPS_TOOL_ANNOTATIONS,
150+
action: (params: BatchGeocodeParams) => BatchGeocode.ACTION(params),
151+
},
144152
],
145153
},
146154
];

src/tools/maps/batchGeocode.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { z } from "zod";
2+
import { PlacesSearcher } from "../../services/PlacesSearcher.js";
3+
import { getCurrentApiKey } from "../../utils/requestContext.js";
4+
5+
const NAME = "maps_batch_geocode";
6+
const DESCRIPTION =
7+
"Geocode multiple addresses in one call — up to 50 addresses, returns coordinates for each. Use when the user provides a list of addresses and needs all their coordinates, e.g. 'geocode these 10 offices' or 'get coordinates for all these restaurants'. For more than 50, use the CLI batch-geocode command instead.";
8+
9+
const SCHEMA = {
10+
addresses: z
11+
.array(z.string())
12+
.min(1)
13+
.max(50)
14+
.describe("List of addresses or landmark names to geocode (max 50)"),
15+
};
16+
17+
export type BatchGeocodeParams = z.infer<z.ZodObject<typeof SCHEMA>>;
18+
19+
async function ACTION(params: any): Promise<{ content: any[]; isError?: boolean }> {
20+
try {
21+
const apiKey = getCurrentApiKey();
22+
const searcher = new PlacesSearcher(apiKey);
23+
const addresses: string[] = params.addresses;
24+
25+
const results = await Promise.all(
26+
addresses.map(async (address: string) => {
27+
try {
28+
const result = await searcher.geocode(address);
29+
return { address, ...result };
30+
} catch (error: any) {
31+
return { address, success: false, error: error.message };
32+
}
33+
})
34+
);
35+
36+
const succeeded = results.filter((r) => r.success).length;
37+
const failed = results.filter((r) => !r.success).length;
38+
39+
return {
40+
content: [
41+
{
42+
type: "text",
43+
text: JSON.stringify({ total: addresses.length, succeeded, failed, results }, null, 2),
44+
},
45+
],
46+
isError: false,
47+
};
48+
} catch (error: any) {
49+
const errorMessage = error instanceof Error ? error.message : JSON.stringify(error);
50+
return {
51+
isError: true,
52+
content: [{ type: "text", text: `Error batch geocoding: ${errorMessage}` }],
53+
};
54+
}
55+
}
56+
57+
export const BatchGeocode = {
58+
NAME,
59+
DESCRIPTION,
60+
SCHEMA,
61+
ACTION,
62+
};

tests/smoke.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@ async function testListTools(session: McpSession): Promise<void> {
212212
"maps_weather",
213213
"maps_air_quality",
214214
"maps_static_map",
215+
"maps_batch_geocode",
215216
];
216217

217218
for (const name of expectedTools) {
@@ -459,6 +460,24 @@ async function testToolCalls(session: McpSession): Promise<void> {
459460
assert(typeof imageContent.data === "string" && imageContent.data.length > 100, "Static map returns base64 data");
460461
}
461462
}
463+
464+
// Test batch geocode
465+
const batchResult = await sendRequest(session, "tools/call", {
466+
name: "maps_batch_geocode",
467+
arguments: { addresses: ["Tokyo Tower", "Eiffel Tower"] },
468+
});
469+
const batchContent = batchResult?.result?.content ?? [];
470+
assert(batchContent.length > 0, "Batch geocode returns content");
471+
if (batchContent.length > 0) {
472+
let valid = false;
473+
try {
474+
const parsed = JSON.parse(batchContent[0].text);
475+
valid = parsed?.total === 2 && parsed?.succeeded === 2 && Array.isArray(parsed?.results);
476+
} catch {
477+
/* ignore parse errors */
478+
}
479+
assert(valid, "Batch geocode returns 2 results with correct counts");
480+
}
462481
}
463482

464483
async function testMultiSession(): Promise<void> {

0 commit comments

Comments
 (0)