Skip to content

Commit 44b5e88

Browse files
feat: implement avoid_highways and avoid_tolls on route planner (#80)
Add drive-only avoid_tolls and avoid_highways options to maps_directions, maps_distance_matrix, and maps_plan_route (service-level routeModifiers with non-driving guard). Also fix CLI exec output truncation for large responses (e.g. static maps) by replacing console.log+process.exit with process.stdout.write+process.exitCode. Co-authored-by: André Herculano <228650+andresilveirah@users.noreply.github.com>
1 parent 444803d commit 44b5e88

10 files changed

Lines changed: 145 additions & 20 deletions

File tree

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,8 @@ Special thanks to [@junyinnnn](https://github.com/junyinnnn) for helping add sup
6868
| `maps_place_details` | Get full details for a place by its place_id — reviews, phone, website, hours. Optional `maxPhotos` param returns photo URLs. |
6969
| `maps_geocode` | Convert an address or landmark name into GPS coordinates. |
7070
| `maps_reverse_geocode` | Convert GPS coordinates into a street address. |
71-
| `maps_distance_matrix` | Calculate travel distances and times between multiple origins and destinations. |
72-
| `maps_directions` | Get step-by-step navigation between two points with route details. |
71+
| `maps_distance_matrix` | Calculate travel distances and times between multiple origins and destinations. Driving mode supports `avoid_tolls` and `avoid_highways`. |
72+
| `maps_directions` | Get step-by-step navigation between two points with route details. Driving mode supports `avoid_tolls` and `avoid_highways`. |
7373
| `maps_elevation` | Get elevation (meters above sea level) for geographic coordinates. |
7474
| `maps_timezone` | Get timezone ID, name, UTC/DST offsets, and local time for coordinates. |
7575
| `maps_weather` | Get current weather conditions or forecast — temperature, humidity, wind, UV, precipitation. |
@@ -79,7 +79,7 @@ Special thanks to [@junyinnnn](https://github.com/junyinnnn) for helping add sup
7979
| `maps_search_along_route` | Search for places along a route between two points — ranked by minimal detour time. |
8080
| **Composite Tools** | |
8181
| `maps_explore_area` | Explore what's around a location — searches multiple place types and gets details in one call. |
82-
| `maps_plan_route` | Plan an optimized multi-stop route — uses Routes API waypoint optimization (up to 25 stops) for efficient ordering. |
82+
| `maps_plan_route` | Plan an optimized multi-stop route — uses Routes API waypoint optimization (up to 25 stops) for efficient ordering. Driving mode supports `avoid_tolls` and `avoid_highways`. |
8383
| `maps_compare_places` | Compare places side-by-side — searches, gets details, and optionally calculates distances. |
8484
| `maps_local_rank_tracker` | Track a business's local search ranking across a geographic grid — like LocalFalcon. Supports up to 3 keywords for batch scanning. Returns rank at each point, top-3 competitors, and metrics (ARP, ATRP, SoLV). |
8585

skills/google-maps/SKILL.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@ Without this Skill, the agent can only guess or refuse when asked "how do I get
5050
### Routing & Distance
5151
| Tool | When to use | Example |
5252
|------|-------------|---------|
53-
| `maps_directions` | How to get from A to B | "Route from Taipei Main Station to the airport" |
54-
| `maps_distance_matrix` | Compare distances across multiple points | "Which of these 3 hotels is closest to the airport?" |
53+
| `maps_directions` | How to get from A to B, including drive-only avoid-tolls/highways controls | "Route from Taipei Main Station to airport avoiding tolls" |
54+
| `maps_distance_matrix` | Compare distances across multiple points, including drive-only avoid-tolls/highways controls | "Which of these 3 hotels is closest to airport without highways?" |
5555
| `maps_search_along_route` | Find places along a route (meals, stops) ranked by detour time | "Restaurants between Fushimi Inari and Kiyomizu-dera" |
5656

5757
### Environment
@@ -71,7 +71,7 @@ Without this Skill, the agent can only guess or refuse when asked "how do I get
7171
| Tool | When to use | Example |
7272
|------|-------------|---------|
7373
| `maps_explore_area` | Overview of a neighborhood | "What's around Tokyo Tower?" |
74-
| `maps_plan_route` | Multi-stop optimized itinerary (Routes API waypoint optimization, up to 25 stops) | "Visit these 5 places efficiently" |
74+
| `maps_plan_route` | Multi-stop optimized itinerary (Routes API waypoint optimization, up to 25 stops) with drive-only avoid-tolls/highways controls | "Visit these 5 places efficiently without tolls" |
7575
| `maps_compare_places` | Side-by-side comparison | "Which ramen shop near Shibuya?" |
7676
| `maps_local_rank_tracker` | Local SEO grid rank tracking | "How does this dentist rank across the area?" |
7777

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,8 @@ exec maps_directions '{"origin": "Tokyo Tower", "destination": "Shibuya Station"
168168
| mode | string | no | Travel mode: driving, walking, bicycling, transit |
169169
| departure_time | string | no | Departure time (ISO 8601 or "now") |
170170
| arrival_time | string | no | Desired arrival time (transit only) |
171+
| avoid_tolls | boolean | no | Avoid toll roads where reasonable. Driving only |
172+
| avoid_highways | boolean | no | Avoid highways where reasonable. Driving only |
171173

172174
---
173175

@@ -186,6 +188,9 @@ exec maps_distance_matrix '{"origins": ["Tokyo Tower"], "destinations": ["Shibuy
186188
| origins | string[] | yes | List of origin addresses |
187189
| destinations | string[] | yes | List of destination addresses |
188190
| mode | string | no | Travel mode: driving, walking, bicycling, transit |
191+
| departure_time | string | no | Departure time in ISO 8601 format for traffic-aware driving estimates |
192+
| avoid_tolls | boolean | no | Avoid toll roads where reasonable. Driving only |
193+
| avoid_highways | boolean | no | Avoid highways where reasonable. Driving only |
189194

190195
---
191196

@@ -377,6 +382,9 @@ exec maps_plan_route '{"stops": ["Tokyo Tower", "Shibuya Station", "Shinjuku Sta
377382
| stops | string[] | yes | Addresses or landmarks (min 2, up to 27 total with origin + destination) |
378383
| mode | string | no | driving, walking, bicycling, transit (default: driving) |
379384
| optimize | boolean | no | Auto-optimize visit order via Routes API (default: true). Not available for transit mode. |
385+
| departure_time | string | no | Departure time in ISO 8601 format for traffic-aware driving routes |
386+
| avoid_tolls | boolean | no | Avoid toll roads where reasonable. Driving only |
387+
| avoid_highways | boolean | no | Avoid highways where reasonable. Driving only |
380388

381389
---
382390

src/cli.ts

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -139,12 +139,21 @@ async function execTool(toolName: string, params: any, apiKey: string): Promise<
139139
params.destination,
140140
params.mode,
141141
params.departure_time,
142-
params.arrival_time
142+
params.arrival_time,
143+
params.avoid_tolls,
144+
params.avoid_highways
143145
);
144146

145147
case "distance-matrix":
146148
case "maps_distance_matrix":
147-
return searcher.calculateDistanceMatrix(params.origins, params.destinations, params.mode, params.departure_time);
149+
return searcher.calculateDistanceMatrix(
150+
params.origins,
151+
params.destinations,
152+
params.mode,
153+
params.departure_time,
154+
params.avoid_tolls,
155+
params.avoid_highways
156+
);
148157

149158
case "elevation":
150159
case "maps_elevation":
@@ -274,25 +283,25 @@ if (isRunDirectly || isMainModule) {
274283
},
275284
async (argv) => {
276285
if (!argv.apikey) {
277-
console.error(
286+
process.stderr.write(
278287
JSON.stringify(
279288
{
280289
error: "GOOGLE_MAPS_API_KEY not set. Use --apikey or set GOOGLE_MAPS_API_KEY environment variable.",
281290
},
282291
null,
283292
2
284-
)
293+
) + "\n"
285294
);
286-
process.exit(1);
295+
process.exitCode = 1;
296+
return;
287297
}
288298
try {
289299
const params = argv.params ? JSON.parse(argv.params as string) : {};
290300
const result = await execTool(argv.tool as string, params, argv.apikey as string);
291-
console.log(JSON.stringify(result, null, 2));
292-
process.exit(0);
301+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
293302
} catch (error: any) {
294-
console.error(JSON.stringify({ error: error.message }, null, 2));
295-
process.exit(1);
303+
process.stderr.write(JSON.stringify({ error: error.message }, null, 2) + "\n");
304+
process.exitCode = 1;
296305
}
297306
}
298307
)

src/services/PlacesSearcher.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -298,14 +298,18 @@ export class PlacesSearcher {
298298
origins: string[],
299299
destinations: string[],
300300
mode: "driving" | "walking" | "bicycling" | "transit" = "driving",
301-
departure_time?: string
301+
departure_time?: string,
302+
avoid_tolls?: boolean,
303+
avoid_highways?: boolean
302304
): Promise<DistanceMatrixResponse> {
303305
try {
304306
const result = await this.routesService.computeRouteMatrix({
305307
origins,
306308
destinations,
307309
mode,
308310
...(departure_time ? { departureTime: new Date(departure_time) } : {}),
311+
...(avoid_tolls !== undefined ? { avoidTolls: avoid_tolls } : {}),
312+
...(avoid_highways !== undefined ? { avoidHighways: avoid_highways } : {}),
309313
});
310314

311315
return {
@@ -325,7 +329,9 @@ export class PlacesSearcher {
325329
destination: string,
326330
mode: "driving" | "walking" | "bicycling" | "transit" = "driving",
327331
departure_time?: string,
328-
arrival_time?: string
332+
arrival_time?: string,
333+
avoid_tolls?: boolean,
334+
avoid_highways?: boolean
329335
): Promise<DirectionsResponse> {
330336
try {
331337
const departureTime = departure_time ? new Date(departure_time) : undefined;
@@ -336,6 +342,8 @@ export class PlacesSearcher {
336342
mode,
337343
...(departureTime ? { departureTime } : {}),
338344
...(arrivalTime ? { arrivalTime } : {}),
345+
...(avoid_tolls !== undefined ? { avoidTolls: avoid_tolls } : {}),
346+
...(avoid_highways !== undefined ? { avoidHighways: avoid_highways } : {}),
339347
});
340348

341349
return {
@@ -495,6 +503,8 @@ export class PlacesSearcher {
495503
mode?: "driving" | "walking" | "bicycling" | "transit";
496504
optimize?: boolean;
497505
departure_time?: string;
506+
avoid_tolls?: boolean;
507+
avoid_highways?: boolean;
498508
}): Promise<any> {
499509
const mode = params.mode || "driving";
500510
const stops = params.stops;
@@ -527,6 +537,8 @@ export class PlacesSearcher {
527537
intermediates,
528538
optimizeWaypointOrder: shouldOptimize,
529539
...(params.departure_time ? { departureTime: new Date(params.departure_time) } : {}),
540+
...(params.avoid_tolls !== undefined ? { avoidTolls: params.avoid_tolls } : {}),
541+
...(params.avoid_highways !== undefined ? { avoidHighways: params.avoid_highways } : {}),
530542
});
531543

532544
const route = routeResult.routes[0];

src/services/RoutesService.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,27 @@ function toWaypoint(location: string): any {
8484
return { address: location };
8585
}
8686

87+
function buildRouteModifiers(params: { avoidTolls?: boolean; avoidHighways?: boolean }, travelMode: string): any {
88+
const routeModifiers: Record<string, boolean> = {};
89+
90+
if (params.avoidTolls) {
91+
routeModifiers.avoidTolls = true;
92+
}
93+
if (params.avoidHighways) {
94+
routeModifiers.avoidHighways = true;
95+
}
96+
97+
if (Object.keys(routeModifiers).length === 0) {
98+
return undefined;
99+
}
100+
101+
if (travelMode !== "DRIVE") {
102+
throw new Error('Route modifiers "avoid_tolls" and "avoid_highways" are only supported with mode "driving".');
103+
}
104+
105+
return routeModifiers;
106+
}
107+
87108
/**
88109
* Routes API REST client.
89110
*
@@ -112,6 +133,8 @@ export class RoutesService {
112133
arrivalTime?: Date;
113134
intermediates?: string[];
114135
optimizeWaypointOrder?: boolean;
136+
avoidTolls?: boolean;
137+
avoidHighways?: boolean;
115138
}): Promise<{
116139
routes: any[];
117140
summary: string;
@@ -135,6 +158,11 @@ export class RoutesService {
135158
requestBody.routingPreference = "TRAFFIC_AWARE";
136159
}
137160

161+
const routeModifiers = buildRouteModifiers(params, travelMode);
162+
if (routeModifiers) {
163+
requestBody.routeModifiers = routeModifiers;
164+
}
165+
138166
// Departure/arrival time (only set if explicitly provided — Routes API rejects past timestamps)
139167
if (params.arrivalTime) {
140168
requestBody.arrivalTime = params.arrivalTime.toISOString();
@@ -219,6 +247,8 @@ export class RoutesService {
219247
destinations: string[];
220248
mode?: string;
221249
departureTime?: Date;
250+
avoidTolls?: boolean;
251+
avoidHighways?: boolean;
222252
}): Promise<{
223253
distances: any[][];
224254
durations: any[][];
@@ -239,6 +269,11 @@ export class RoutesService {
239269
requestBody.routingPreference = "TRAFFIC_AWARE";
240270
}
241271

272+
const routeModifiers = buildRouteModifiers(params, travelMode);
273+
if (routeModifiers) {
274+
requestBody.routeModifiers = routeModifiers;
275+
}
276+
242277
if (params.departureTime) {
243278
requestBody.departureTime = params.departureTime.toISOString();
244279
}

src/tools/maps/directions.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ const SCHEMA = {
1515
.describe("Travel mode for directions"),
1616
departure_time: z.string().optional().describe("Departure time (ISO string format)"),
1717
arrival_time: z.string().optional().describe("Arrival time (ISO string format)"),
18+
avoid_tolls: z.boolean().optional().describe('Avoid toll roads where reasonable. Only supported with mode "driving".'),
19+
avoid_highways: z
20+
.boolean()
21+
.optional()
22+
.describe('Avoid highways where reasonable. Only supported with mode "driving".'),
1823
};
1924

2025
export type DirectionsParams = z.infer<z.ZodObject<typeof SCHEMA>>;
@@ -29,7 +34,9 @@ async function ACTION(params: any): Promise<{ content: any[]; isError?: boolean
2934
params.destination,
3035
params.mode,
3136
params.departure_time,
32-
params.arrival_time
37+
params.arrival_time,
38+
params.avoid_tolls,
39+
params.avoid_highways
3340
);
3441

3542
if (!result.success) {

src/tools/maps/distanceMatrix.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ const SCHEMA = {
1919
.describe(
2020
"Departure time in ISO 8601 format (e.g. 2026-03-21T09:00:00Z). Enables traffic-aware duration estimates."
2121
),
22+
avoid_tolls: z.boolean().optional().describe('Avoid toll roads where reasonable. Only supported with mode "driving".'),
23+
avoid_highways: z
24+
.boolean()
25+
.optional()
26+
.describe('Avoid highways where reasonable. Only supported with mode "driving".'),
2227
};
2328

2429
export type DistanceMatrixParams = z.infer<z.ZodObject<typeof SCHEMA>>;
@@ -32,7 +37,9 @@ async function ACTION(params: any): Promise<{ content: any[]; isError?: boolean
3237
params.origins,
3338
params.destinations,
3439
params.mode,
35-
params.departure_time
40+
params.departure_time,
41+
params.avoid_tolls,
42+
params.avoid_highways
3643
);
3744

3845
if (!result.success) {

src/tools/maps/planRoute.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ const SCHEMA = {
1919
.string()
2020
.optional()
2121
.describe("Departure time in ISO 8601 format (e.g. 2026-03-21T09:00:00Z). Enables traffic-aware routing."),
22+
avoid_tolls: z.boolean().optional().describe('Avoid toll roads where reasonable. Only supported with mode "driving".'),
23+
avoid_highways: z
24+
.boolean()
25+
.optional()
26+
.describe('Avoid highways where reasonable. Only supported with mode "driving".'),
2227
};
2328

2429
export type PlanRouteParams = z.infer<z.ZodObject<typeof SCHEMA>>;

tests/smoke.test.ts

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,17 +243,36 @@ async function testListTools(session: McpSession): Promise<void> {
243243
}
244244
}
245245

246-
// Verify departure_time parameter exists in plan_route and distance_matrix
246+
// Verify routing parameters exist in route tools
247247
const planRoute = tools.find((t: any) => t.name === "maps_plan_route");
248248
assert(
249249
planRoute?.inputSchema?.properties?.departure_time !== undefined,
250250
"maps_plan_route has departure_time parameter"
251251
);
252+
assert(planRoute?.inputSchema?.properties?.avoid_tolls !== undefined, "maps_plan_route has avoid_tolls parameter");
253+
assert(
254+
planRoute?.inputSchema?.properties?.avoid_highways !== undefined,
255+
"maps_plan_route has avoid_highways parameter"
256+
);
257+
const directions = tools.find((t: any) => t.name === "maps_directions");
258+
assert(directions?.inputSchema?.properties?.avoid_tolls !== undefined, "maps_directions has avoid_tolls parameter");
259+
assert(
260+
directions?.inputSchema?.properties?.avoid_highways !== undefined,
261+
"maps_directions has avoid_highways parameter"
262+
);
252263
const distMatrix = tools.find((t: any) => t.name === "maps_distance_matrix");
253264
assert(
254265
distMatrix?.inputSchema?.properties?.departure_time !== undefined,
255266
"maps_distance_matrix has departure_time parameter"
256267
);
268+
assert(
269+
distMatrix?.inputSchema?.properties?.avoid_tolls !== undefined,
270+
"maps_distance_matrix has avoid_tolls parameter"
271+
);
272+
assert(
273+
distMatrix?.inputSchema?.properties?.avoid_highways !== undefined,
274+
"maps_distance_matrix has avoid_highways parameter"
275+
);
257276
}
258277

259278
async function testGeocode(session: McpSession): Promise<void> {
@@ -850,6 +869,7 @@ async function testExecMode(): Promise<void> {
850869
return execFileSync("node", [cliPath, "exec", tool, params, "--apikey", API_KEY], {
851870
encoding: "utf-8",
852871
timeout: 30000,
872+
maxBuffer: 10 * 1024 * 1024,
853873
}).trim();
854874
} catch (err: any) {
855875
return err.stdout?.trim() ?? err.message;
@@ -1059,6 +1079,28 @@ async function testTransitErrorMessages(session: McpSession): Promise<void> {
10591079
}
10601080
}
10611081
}
1082+
1083+
const avoidResult = await sendRequest(session, "tools/call", {
1084+
name: "maps_directions",
1085+
arguments: {
1086+
origin: "Tokyo Station",
1087+
destination: "Nagoya Station",
1088+
mode: "walking",
1089+
avoid_highways: true,
1090+
},
1091+
});
1092+
const avoidContent = avoidResult?.result?.content ?? [];
1093+
assert(avoidContent.length > 0, "Invalid route modifiers returns content");
1094+
if (avoidContent.length > 0) {
1095+
const text = avoidContent[0]?.text ?? "";
1096+
const isError = avoidResult?.result?.isError === true;
1097+
assert(isError, "Non-driving route modifiers return isError=true");
1098+
assert(
1099+
text.includes('only supported with mode "driving"'),
1100+
"Non-driving route modifiers return clear error",
1101+
`got: ${text.slice(0, 200)}`
1102+
);
1103+
}
10621104
}
10631105

10641106
// --------------- Main ---------------

0 commit comments

Comments
 (0)