diff --git a/assets/mapbox-logo-black.svg b/assets/mapbox-logo-black.svg
new file mode 100644
index 0000000..a0cbd94
--- /dev/null
+++ b/assets/mapbox-logo-black.svg
@@ -0,0 +1,38 @@
+
+
+
diff --git a/assets/mapbox-logo-white.svg b/assets/mapbox-logo-white.svg
new file mode 100644
index 0000000..8d62aef
--- /dev/null
+++ b/assets/mapbox-logo-white.svg
@@ -0,0 +1,42 @@
+
+
+
diff --git a/src/index.ts b/src/index.ts
index 50d53a7..c4313fe 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -66,7 +66,21 @@ const allResources = getAllResources();
const server = new McpServer(
{
name: versionInfo.name,
- version: versionInfo.version
+ version: versionInfo.version,
+ icons: [
+ {
+ src: 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDIxLjAuMiwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9Im5ldyIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeD0iMHB4IiB5PSIwcHgiCgkgdmlld0JveD0iMCAwIDgwMCAxODAiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDgwMCAxODA7IiB4bWw6c3BhY2U9InByZXNlcnZlIj4KPHRpdGxlPk1hcGJveF9Mb2dvXzA4PC90aXRsZT4KPGc+Cgk8Zz4KCQk8cGF0aCBkPSJNNTk0LjYsNDkuOGMtOS45LDAtMTkuNCw0LjEtMjYuMywxMS4zVjIzYzAtMS4yLTEtMi4yLTIuMi0yLjJsMCwwaC0xMy40Yy0xLjIsMC0yLjIsMS0yLjIsMi4ydjEwM2MwLDEuMiwxLDIuMiwyLjIsMi4yCgkJCWgxMy40YzEuMiwwLDIuMi0xLDIuMi0yLjJ2MHYtNy4xYzYuOSw3LjIsMTYuMywxMS4zLDI2LjMsMTEuM2MyMC45LDAsMzcuOC0xOCwzNy44LTQwLjJTNjE1LjUsNDkuOCw1OTQuNiw0OS44eiBNNTkxLjUsMTE0LjEKCQkJYy0xMi43LDAtMjMtMTAuNi0yMy4xLTIzLjh2LTAuNmMwLjItMTMuMiwxMC40LTIzLjgsMjMuMS0yMy44YzEyLjgsMCwyMy4xLDEwLjgsMjMuMSwyNC4xUzYwNC4yLDExNC4xLDU5MS41LDExNC4xTDU5MS41LDExNC4xeiIKCQkJLz4KCQk8cGF0aCBkPSJNNjgxLjcsNDkuOGMtMjIuNiwwLTQwLjksMTgtNDAuOSw0MC4yczE4LjMsNDAuMiw0MC45LDQwLjJjMjIuNiwwLDQwLjktMTgsNDAuOS00MC4yUzcwNC4zLDQ5LjgsNjgxLjcsNDkuOHoKCQkJIE02ODEuNiwxMTQuMWMtMTIuOCwwLTIzLjEtMTAuOC0yMy4xLTI0LjFzMTAuNC0yNC4xLDIzLjEtMjQuMXMyMy4xLDEwLjgsMjMuMSwyNC4xUzY5NC4zLDExNC4xLDY4MS42LDExNC4xTDY4MS42LDExNC4xeiIvPgoJCTxwYXRoIGQ9Ik00MzEuNiw1MS44aC0xMy40Yy0xLjIsMC0yLjIsMS0yLjIsMi4yYzAsMCwwLDAsMCwwdjcuMWMtNi45LTcuMi0xNi4zLTExLjMtMjYuMy0xMS4zYy0yMC45LDAtMzcuOCwxOC0zNy44LDQwLjIKCQkJczE2LjksNDAuMiwzNy44LDQwLjJjOS45LDAsMTkuNC00LjEsMjYuMy0xMS4zdjcuMWMwLDEuMiwxLDIuMiwyLjIsMi4ybDAsMGgxMy40YzEuMiwwLDIuMi0xLDIuMi0yLjJ2MFY1NAoJCQlDNDMzLjgsNTIuOCw0MzIuOCw1MS44LDQzMS42LDUxLjh6IE0zOTIuOCwxMTQuMWMtMTIuOCwwLTIzLjEtMTAuOC0yMy4xLTI0LjFzMTAuNC0yNC4xLDIzLjEtMjQuMWMxMi43LDAsMjMsMTAuNiwyMy4xLDIzLjh2MC42CgkJCUM0MTUuOCwxMDMuNSw0MDUuNSwxMTQuMSwzOTIuOCwxMTQuMUwzOTIuOCwxMTQuMXoiLz4KCQk8cGF0aCBkPSJNNDk4LjUsNDkuOGMtOS45LDAtMTkuNCw0LjEtMjYuMywxMS4zVjU0YzAtMS4yLTEtMi4yLTIuMi0yLjJsMCwwaC0xMy40Yy0xLjIsMC0yLjIsMS0yLjIsMi4yYzAsMCwwLDAsMCwwdjEwMwoJCQljMCwxLjIsMSwyLjIsMi4yLDIuMmwwLDBoMTMuNGMxLjIsMCwyLjItMSwyLjItMi4ydjB2LTM4LjFjNi45LDcuMiwxNi4zLDExLjMsMjYuMywxMS4zYzIwLjksMCwzNy44LTE4LDM3LjgtNDAuMgoJCQlTNTE5LjQsNDkuOCw0OTguNSw0OS44eiBNNDk1LjQsMTE0LjFjLTEyLjcsMC0yMy0xMC42LTIzLjEtMjMuOHYtMC42YzAuMi0xMy4yLDEwLjQtMjMuOCwyMy4xLTIzLjhjMTIuOCwwLDIzLjEsMTAuOCwyMy4xLDI0LjEKCQkJUzUwOC4yLDExNC4xLDQ5NS40LDExNC4xTDQ5NS40LDExNC4xeiIvPgoJCTxwYXRoIGQ9Ik0zMTEuOCw0OS44Yy0xMCwwLjEtMTkuMSw1LjktMjMuNCwxNWMtNC45LTkuMy0xNC43LTE1LjEtMjUuMi0xNWMtOC4yLDAtMTUuOSw0LTIwLjcsMTAuNlY1NGMwLTEuMi0xLTIuMi0yLjItMi4ybDAsMAoJCQloLTEzLjRjLTEuMiwwLTIuMiwxLTIuMiwyLjJjMCwwLDAsMCwwLDB2NzJjMCwxLjIsMSwyLjIsMi4yLDIuMmgwaDEzLjRjMS4yLDAsMi4yLTEsMi4yLTIuMnYwVjgyLjljMC41LTkuNiw3LjItMTcuMywxNS40LTE3LjMKCQkJYzguNSwwLDE1LjYsNy4xLDE1LjYsMTYuNHY0NGMwLDEuMiwxLDIuMiwyLjIsMi4ybDEzLjUsMGMxLjIsMCwyLjItMSwyLjItMi4yYzAsMCwwLDAsMCwwbC0wLjEtNDQuOGMxLjItOC44LDcuNS0xNS42LDE1LjItMTUuNgoJCQljOC41LDAsMTUuNiw3LjEsMTUuNiwxNi40djQ0YzAsMS4yLDEsMi4yLDIuMiwyLjJsMTMuNSwwYzEuMiwwLDIuMi0xLDIuMi0yLjJjMCwwLDAsMCwwLDBsLTAuMS00OS41CgkJCUMzMzkuOSw2MS43LDMyNy4zLDQ5LjgsMzExLjgsNDkuOHoiLz4KCQk8cGF0aCBkPSJNNzk0LjcsMTI1LjFsLTIzLjItMzUuM2wyMy0zNWMwLjYtMC45LDAuMy0yLjItMC42LTIuOGMtMC4zLTAuMi0wLjctMC4zLTEuMS0wLjNoLTE1LjVjLTEuMiwwLTIuMywwLjYtMi45LDEuNkw3NjAuOSw3NgoJCQlsLTEzLjUtMjIuNmMtMC42LTEtMS43LTEuNi0yLjktMS42aC0xNS41Yy0xLjEsMC0yLDAuOS0yLDJjMCwwLjQsMC4xLDAuOCwwLjMsMS4xbDIzLDM1bC0yMy4yLDM1LjNjLTAuNiwwLjktMC4zLDIuMiwwLjYsMi44CgkJCWMwLjMsMC4yLDAuNywwLjMsMS4xLDAuM2gxNS41YzEuMiwwLDIuMy0wLjYsMi45LTEuNmwxMy44LTIzbDEzLjgsMjNjMC42LDEsMS43LDEuNiwyLjksMS42SDc5M2MxLjEsMCwyLTAuOSwyLTIKCQkJQzc5NSwxMjUuOSw3OTQuOSwxMjUuNSw3OTQuNywxMjUuMXoiLz4KCTwvZz4KCTxnPgoJCTxwYXRoIGQ9Ik05My45LDEuMUM0NC44LDEuMSw1LDQwLjksNSw5MHMzOS44LDg4LjksODguOSw4OC45czg4LjktMzkuOCw4OC45LTg4LjlDMTgyLjgsNDAuOSwxNDMsMS4xLDkzLjksMS4xeiBNMTM2LjEsMTExLjgKCQkJYy0zMC40LDMwLjQtODQuNywyMC43LTg0LjcsMjAuN3MtOS44LTU0LjIsMjAuNy04NC43Qzg5LDMwLjksMTE3LDMxLjYsMTM0LjcsNDkuMlMxNTMsOTQuOSwxMzYuMSwxMTEuOEwxMzYuMSwxMTEuOHoiLz4KCQk8cG9seWdvbiBwb2ludHM9IjEwNC4xLDUzLjIgOTUuNCw3MS4xIDc3LjUsNzkuOCA5NS40LDg4LjUgMTA0LjEsMTA2LjQgMTEyLjgsODguNSAxMzAuNyw3OS44IDExMi44LDcxLjEgCQkiLz4KCTwvZz4KPC9nPgo8L3N2Zz4K',
+ mimeType: 'image/svg+xml',
+ sizes: ['800x180'],
+ theme: 'light'
+ },
+ {
+ src: 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDIxLjAuMiwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9Im5ldyIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeD0iMHB4IiB5PSIwcHgiCgkgdmlld0JveD0iMCAwIDgwMCAxODAiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDgwMCAxODA7IiB4bWw6c3BhY2U9InByZXNlcnZlIj4KPHN0eWxlIHR5cGU9InRleHQvY3NzIj4KCS5zdDB7ZmlsbDojRkZGRkZGO30KPC9zdHlsZT4KPHRpdGxlPk1hcGJveF9Mb2dvXzA4PC90aXRsZT4KPGc+Cgk8Zz4KCQk8cGF0aCBjbGFzcz0ic3QwIiBkPSJNNTk0LjYsNDkuOGMtOS45LDAtMTkuNCw0LjEtMjYuMywxMS4zVjIzYzAtMS4yLTEtMi4yLTIuMi0yLjJsMCwwaC0xMy40Yy0xLjIsMC0yLjIsMS0yLjIsMi4ydjEwMwoJCQljMCwxLjIsMSwyLjIsMi4yLDIuMmgxMy40YzEuMiwwLDIuMi0xLDIuMi0yLjJ2MHYtNy4xYzYuOSw3LjIsMTYuMywxMS4zLDI2LjMsMTEuM2MyMC45LDAsMzcuOC0xOCwzNy44LTQwLjIKCQkJUzYxNS41LDQ5LjgsNTk0LjYsNDkuOHogTTU5MS41LDExNC4xYy0xMi43LDAtMjMtMTAuNi0yMy4xLTIzLjh2LTAuNmMwLjItMTMuMiwxMC40LTIzLjgsMjMuMS0yMy44YzEyLjgsMCwyMy4xLDEwLjgsMjMuMSwyNC4xCgkJCVM2MDQuMiwxMTQuMSw1OTEuNSwxMTQuMUw1OTEuNSwxMTQuMXoiLz4KCQk8cGF0aCBjbGFzcz0ic3QwIiBkPSJNNjgxLjcsNDkuOGMtMjIuNiwwLTQwLjksMTgtNDAuOSw0MC4yczE4LjMsNDAuMiw0MC45LDQwLjJjMjIuNiwwLDQwLjktMTgsNDAuOS00MC4yUzcwNC4zLDQ5LjgsNjgxLjcsNDkuOHoKCQkJIE02ODEuNiwxMTQuMWMtMTIuOCwwLTIzLjEtMTAuOC0yMy4xLTI0LjFzMTAuNC0yNC4xLDIzLjEtMjQuMXMyMy4xLDEwLjgsMjMuMSwyNC4xUzY5NC4zLDExNC4xLDY4MS42LDExNC4xTDY4MS42LDExNC4xeiIvPgoJCTxwYXRoIGNsYXNzPSJzdDAiIGQ9Ik00MzEuNiw1MS44aC0xMy40Yy0xLjIsMC0yLjIsMS0yLjIsMi4yYzAsMCwwLDAsMCwwdjcuMWMtNi45LTcuMi0xNi4zLTExLjMtMjYuMy0xMS4zCgkJCWMtMjAuOSwwLTM3LjgsMTgtMzcuOCw0MC4yczE2LjksNDAuMiwzNy44LDQwLjJjOS45LDAsMTkuNC00LjEsMjYuMy0xMS4zdjcuMWMwLDEuMiwxLDIuMiwyLjIsMi4ybDAsMGgxMy40YzEuMiwwLDIuMi0xLDIuMi0yLjIKCQkJdjBWNTRDNDMzLjgsNTIuOCw0MzIuOCw1MS44LDQzMS42LDUxLjh6IE0zOTIuOCwxMTQuMWMtMTIuOCwwLTIzLjEtMTAuOC0yMy4xLTI0LjFzMTAuNC0yNC4xLDIzLjEtMjQuMWMxMi43LDAsMjMsMTAuNiwyMy4xLDIzLjgKCQkJdjAuNkM0MTUuOCwxMDMuNSw0MDUuNSwxMTQuMSwzOTIuOCwxMTQuMUwzOTIuOCwxMTQuMXoiLz4KCQk8cGF0aCBjbGFzcz0ic3QwIiBkPSJNNDk4LjUsNDkuOGMtOS45LDAtMTkuNCw0LjEtMjYuMywxMS4zVjU0YzAtMS4yLTEtMi4yLTIuMi0yLjJsMCwwaC0xMy40Yy0xLjIsMC0yLjIsMS0yLjIsMi4yYzAsMCwwLDAsMCwwCgkJCXYxMDNjMCwxLjIsMSwyLjIsMi4yLDIuMmwwLDBoMTMuNGMxLjIsMCwyLjItMSwyLjItMi4ydjB2LTM4LjFjNi45LDcuMiwxNi4zLDExLjMsMjYuMywxMS4zYzIwLjksMCwzNy44LTE4LDM3LjgtNDAuMgoJCQlTNTE5LjQsNDkuOCw0OTguNSw0OS44eiBNNDk1LjQsMTE0LjFjLTEyLjcsMC0yMy0xMC42LTIzLjEtMjMuOHYtMC42YzAuMi0xMy4yLDEwLjQtMjMuOCwyMy4xLTIzLjhjMTIuOCwwLDIzLjEsMTAuOCwyMy4xLDI0LjEKCQkJUzUwOC4yLDExNC4xLDQ5NS40LDExNC4xTDQ5NS40LDExNC4xeiIvPgoJCTxwYXRoIGNsYXNzPSJzdDAiIGQ9Ik0zMTEuOCw0OS44Yy0xMCwwLjEtMTkuMSw1LjktMjMuNCwxNWMtNC45LTkuMy0xNC43LTE1LjEtMjUuMi0xNWMtOC4yLDAtMTUuOSw0LTIwLjcsMTAuNlY1NAoJCQljMC0xLjItMS0yLjItMi4yLTIuMmwwLDBoLTEzLjRjLTEuMiwwLTIuMiwxLTIuMiwyLjJjMCwwLDAsMCwwLDB2NzJjMCwxLjIsMSwyLjIsMi4yLDIuMmgwaDEzLjRjMS4yLDAsMi4yLTEsMi4yLTIuMnYwVjgyLjkKCQkJYzAuNS05LjYsNy4yLTE3LjMsMTUuNC0xNy4zYzguNSwwLDE1LjYsNy4xLDE1LjYsMTYuNHY0NGMwLDEuMiwxLDIuMiwyLjIsMi4ybDEzLjUsMGMxLjIsMCwyLjItMSwyLjItMi4yYzAsMCwwLDAsMCwwbC0wLjEtNDQuOAoJCQljMS4yLTguOCw3LjUtMTUuNiwxNS4yLTE1LjZjOC41LDAsMTUuNiw3LjEsMTUuNiwxNi40djQ0YzAsMS4yLDEsMi4yLDIuMiwyLjJsMTMuNSwwYzEuMiwwLDIuMi0xLDIuMi0yLjJjMCwwLDAsMCwwLDBsLTAuMS00OS41CgkJCUMzMzkuOSw2MS43LDMyNy4zLDQ5LjgsMzExLjgsNDkuOHoiLz4KCQk8cGF0aCBjbGFzcz0ic3QwIiBkPSJNNzk0LjcsMTI1LjFsLTIzLjItMzUuM2wyMy0zNWMwLjYtMC45LDAuMy0yLjItMC42LTIuOGMtMC4zLTAuMi0wLjctMC4zLTEuMS0wLjNoLTE1LjUKCQkJYy0xLjIsMC0yLjMsMC42LTIuOSwxLjZMNzYwLjksNzZsLTEzLjUtMjIuNmMtMC42LTEtMS43LTEuNi0yLjktMS42aC0xNS41Yy0xLjEsMC0yLDAuOS0yLDJjMCwwLjQsMC4xLDAuOCwwLjMsMS4xbDIzLDM1CgkJCWwtMjMuMiwzNS4zYy0wLjYsMC45LTAuMywyLjIsMC42LDIuOGMwLjMsMC4yLDAuNywwLjMsMS4xLDAuM2gxNS41YzEuMiwwLDIuMy0wLjYsMi45LTEuNmwxMy44LTIzbDEzLjgsMjNjMC42LDEsMS43LDEuNiwyLjksMS42CgkJCUg3OTNjMS4xLDAsMi0wLjksMi0yQzc5NSwxMjUuOSw3OTQuOSwxMjUuNSw3OTQuNywxMjUuMXoiLz4KCTwvZz4KCTxnPgoJCTxwYXRoIGNsYXNzPSJzdDAiIGQ9Ik05My45LDEuMUM0NC44LDEuMSw1LDQwLjksNSw5MHMzOS44LDg4LjksODguOSw4OC45czg4LjktMzkuOCw4OC45LTg4LjlDMTgyLjgsNDAuOSwxNDMsMS4xLDkzLjksMS4xegoJCQkgTTEzNi4xLDExMS44Yy0zMC40LDMwLjQtODQuNywyMC43LTg0LjcsMjAuN3MtOS44LTU0LjIsMjAuNy04NC43Qzg5LDMwLjksMTE3LDMxLjYsMTM0LjcsNDkuMlMxNTMsOTQuOSwxMzYuMSwxMTEuOEwxMzYuMSwxMTEuOAoJCQl6Ii8+CgkJPHBvbHlnb24gY2xhc3M9InN0MCIgcG9pbnRzPSIxMDQuMSw1My4yIDk1LjQsNzEuMSA3Ny41LDc5LjggOTUuNCw4OC41IDEwNC4xLDEwNi40IDExMi44LDg4LjUgMTMwLjcsNzkuOCAxMTIuOCw3MS4xIAkJIi8+Cgk8L2c+CjwvZz4KPC9zdmc+Cg==',
+ mimeType: 'image/svg+xml',
+ sizes: ['800x180'],
+ theme: 'dark'
+ }
+ ]
},
{
capabilities: {
diff --git a/src/tools/directions-tool/DirectionsTool.output.schema.ts b/src/tools/directions-tool/DirectionsTool.output.schema.ts
index 0523025..3e4016d 100644
--- a/src/tools/directions-tool/DirectionsTool.output.schema.ts
+++ b/src/tools/directions-tool/DirectionsTool.output.schema.ts
@@ -284,8 +284,8 @@ const RouteStepSchema = z.object({
distance: z.number(),
duration: z.number(),
weight: z.number(),
- duration_typical: z.number().optional(),
- weight_typical: z.number().optional(),
+ duration_typical: z.number().nullable().optional(),
+ weight_typical: z.number().nullable().optional(),
geometry: z.union([z.string(), GeoJSONLineStringSchema]),
name: z.string(),
ref: z.string().optional(),
@@ -315,8 +315,8 @@ const RouteLegSchema = z.object({
distance: z.number(),
duration: z.number(),
weight: z.number(),
- duration_typical: z.number().optional(),
- weight_typical: z.number().optional(),
+ duration_typical: z.number().nullable().optional(),
+ weight_typical: z.number().nullable().optional(),
steps: z.array(RouteStepSchema),
summary: z.string(),
admins: z.array(AdminSchema),
@@ -333,8 +333,8 @@ const RouteSchema = z.object({
distance: z.number(),
weight_name: z.enum(['auto', 'pedestrian']).optional(), // Removed by cleanResponseData
weight: z.number().optional(), // Removed by cleanResponseData
- duration_typical: z.number().optional(),
- weight_typical: z.number().optional(),
+ duration_typical: z.number().nullable().optional(),
+ weight_typical: z.number().nullable().optional(),
geometry: z.union([z.string(), GeoJSONLineStringSchema]).optional(), // Can be removed when geometries='none'
legs: z.array(RouteLegSchema).optional(), // Removed by cleanResponseData, replaced with leg_summaries
voiceLocale: z.string().optional(),
diff --git a/src/tools/directions-tool/DirectionsTool.ts b/src/tools/directions-tool/DirectionsTool.ts
index 16fa0bc..37fd31f 100644
--- a/src/tools/directions-tool/DirectionsTool.ts
+++ b/src/tools/directions-tool/DirectionsTool.ts
@@ -38,10 +38,117 @@ export class DirectionsTool extends MapboxApiBasedTool<
httpRequest: params.httpRequest
});
}
+ private formatDuration(seconds: number): string {
+ const hours = Math.floor(seconds / 3600);
+ const minutes = Math.round((seconds % 3600) / 60);
+ if (hours > 0) {
+ return `${hours}h ${minutes}min`;
+ }
+ return `${minutes}min`;
+ }
+
+ private formatDistance(meters: number): string {
+ const km = (meters / 1000).toFixed(1);
+ const miles = (meters / 1609.34).toFixed(1);
+ return `${km}km (${miles}mi)`;
+ }
+
protected async execute(
input: z.infer,
accessToken: string
): Promise {
+ // Stage 1: Elicit routing preferences if server available and no preferences set
+ const excludeOptions: string[] = [];
+ if (
+ this.server &&
+ !input.exclude &&
+ input.coordinates.length === 2 // Only for simple A-to-B routes
+ ) {
+ try {
+ const isDrivingProfile =
+ input.routing_profile === 'mapbox/driving-traffic' ||
+ input.routing_profile === 'mapbox/driving';
+
+ const preferenceOptions = [
+ {
+ value: 'fastest',
+ label: 'Fastest route (tolls OK)'
+ },
+ {
+ value: 'avoid_tolls',
+ label: 'Avoid tolls (may be slower)'
+ }
+ ];
+
+ if (isDrivingProfile) {
+ preferenceOptions.push({
+ value: 'avoid_highways',
+ label: 'Avoid highways/motorways'
+ });
+ }
+
+ preferenceOptions.push({
+ value: 'avoid_ferries',
+ label: 'Avoid ferries'
+ });
+
+ const preferencesResult = await this.server.server.elicitInput({
+ mode: 'form',
+ message: 'Choose your routing preferences:',
+ requestedSchema: {
+ type: 'object',
+ properties: {
+ routePreference: {
+ type: 'string',
+ title: 'Route Preference',
+ description: 'Select your routing priorities',
+ enum: preferenceOptions.map((o) => o.value),
+ enumNames: preferenceOptions.map((o) => o.label)
+ }
+ },
+ required: ['routePreference']
+ }
+ });
+
+ if (
+ preferencesResult.action === 'accept' &&
+ preferencesResult.content?.routePreference
+ ) {
+ const preference =
+ typeof preferencesResult.content.routePreference === 'string'
+ ? preferencesResult.content.routePreference
+ : String(preferencesResult.content.routePreference);
+
+ // Map preferences to exclude options
+ if (preference === 'avoid_tolls') {
+ excludeOptions.push('toll', 'cash_only_tolls');
+ } else if (preference === 'avoid_highways' && isDrivingProfile) {
+ excludeOptions.push('motorway');
+ } else if (preference === 'avoid_ferries') {
+ excludeOptions.push('ferry');
+ }
+
+ // Force alternatives=true to get multiple routes for Stage 2
+ input.alternatives = true;
+
+ this.log(
+ 'info',
+ `DirectionsTool: User selected preference: ${preference}, exclude: ${excludeOptions.join(',')}`
+ );
+ }
+ } catch (elicitError) {
+ this.log(
+ 'warning',
+ `DirectionsTool: Stage 1 elicitation failed: ${elicitError instanceof Error ? elicitError.message : 'Unknown error'}`
+ );
+ }
+ }
+
+ // Apply collected exclusions
+ if (excludeOptions.length > 0 && !input.exclude) {
+ input.exclude = excludeOptions.join(',');
+ }
+
// Validate exclude parameter against the actual routing_profile
// This is needed because some exclusions are only driving specific
if (input.exclude) {
@@ -268,6 +375,116 @@ export class DirectionsTool extends MapboxApiBasedTool<
validatedData = cleanedData as DirectionsResponse;
}
+ // Stage 2: Elicit route selection if multiple routes returned
+ if (
+ this.server &&
+ validatedData.routes &&
+ validatedData.routes.length >= 2
+ ) {
+ try {
+ const routeOptions = validatedData.routes.map((route, index) => {
+ const duration = this.formatDuration(route.duration);
+ const distance = this.formatDistance(route.distance);
+ const roads =
+ route.leg_summaries && route.leg_summaries.length > 0
+ ? route.leg_summaries[0]
+ : 'Route';
+
+ // Build traffic/congestion summary
+ let trafficInfo = '';
+ if (route.congestion_information) {
+ const congestion = route.congestion_information;
+ const totalLength =
+ congestion.length_low +
+ congestion.length_moderate +
+ congestion.length_heavy +
+ congestion.length_severe;
+ const heavyPercent = Math.round(
+ ((congestion.length_heavy + congestion.length_severe) /
+ totalLength) *
+ 100
+ );
+ if (heavyPercent > 20) {
+ trafficInfo = ` ⚠️ Heavy traffic (${heavyPercent}%)`;
+ } else if (congestion.length_moderate > 0) {
+ trafficInfo = ' 🟡 Moderate traffic';
+ } else {
+ trafficInfo = ' ✅ Light traffic';
+ }
+ }
+
+ // Count incidents
+ const incidentCount = route.incidents_summary?.length || 0;
+ const incidentInfo =
+ incidentCount > 0 ? ` • ${incidentCount} incident(s)` : '';
+
+ return {
+ value: String(index),
+ label: `${duration} via ${roads} • ${distance}${trafficInfo}${incidentInfo}`
+ };
+ });
+
+ const routeSelectionResult = await this.server.server.elicitInput({
+ mode: 'form',
+ message: `Found ${validatedData.routes.length} routes. Choose your preferred route:`,
+ requestedSchema: {
+ type: 'object',
+ properties: {
+ selectedRoute: {
+ type: 'string',
+ title: 'Select Route',
+ description: 'Choose the route that best fits your needs',
+ enum: routeOptions.map((o) => o.value),
+ enumNames: routeOptions.map((o) => o.label)
+ }
+ },
+ required: ['selectedRoute']
+ }
+ });
+
+ if (
+ routeSelectionResult.action === 'accept' &&
+ routeSelectionResult.content?.selectedRoute
+ ) {
+ const selectedIndexStr =
+ typeof routeSelectionResult.content.selectedRoute === 'string'
+ ? routeSelectionResult.content.selectedRoute
+ : String(routeSelectionResult.content.selectedRoute);
+ const selectedIndex = parseInt(selectedIndexStr, 10);
+ const selectedRoute = validatedData.routes[selectedIndex];
+
+ // Return only the selected route
+ const singleRouteResult: DirectionsResponse = {
+ ...validatedData,
+ routes: [selectedRoute]
+ };
+
+ this.log(
+ 'info',
+ `DirectionsTool: User selected route ${selectedIndex}: ${this.formatDuration(selectedRoute.duration)} via ${selectedRoute.leg_summaries?.[0] || 'route'}`
+ );
+
+ return {
+ content: [
+ { type: 'text', text: JSON.stringify(singleRouteResult, null, 2) }
+ ],
+ structuredContent: singleRouteResult,
+ isError: false
+ };
+ } else if (routeSelectionResult.action === 'decline') {
+ this.log(
+ 'info',
+ 'DirectionsTool: User declined to select a specific route'
+ );
+ }
+ } catch (elicitError) {
+ this.log(
+ 'warning',
+ `DirectionsTool: Stage 2 elicitation failed: ${elicitError instanceof Error ? elicitError.message : 'Unknown error'}`
+ );
+ }
+ }
+
return {
content: [{ type: 'text', text: JSON.stringify(validatedData, null, 2) }],
structuredContent: validatedData,
diff --git a/src/tools/directions-tool/cleanResponseData.ts b/src/tools/directions-tool/cleanResponseData.ts
index 6cbefbb..9078724 100644
--- a/src/tools/directions-tool/cleanResponseData.ts
+++ b/src/tools/directions-tool/cleanResponseData.ts
@@ -67,8 +67,8 @@ interface RawRoute {
distance?: number;
weight_name?: string;
weight?: number;
- duration_typical?: number;
- weight_typical?: number;
+ duration_typical?: number | null;
+ weight_typical?: number | null;
geometry?: unknown;
legs?: RawLeg[];
[key: string]: unknown;
diff --git a/test/tools/directions-tool/DirectionsTool.test.ts b/test/tools/directions-tool/DirectionsTool.test.ts
index 888d088..37c41b7 100644
--- a/test/tools/directions-tool/DirectionsTool.test.ts
+++ b/test/tools/directions-tool/DirectionsTool.test.ts
@@ -1064,4 +1064,155 @@ describe('DirectionsTool', () => {
isError: true
});
});
+
+ describe('elicitation behavior', () => {
+ it('does not elicit when exclude parameter is already provided', async () => {
+ const { httpRequest, mockHttpRequest } = setupHttpRequest();
+
+ await new DirectionsTool({ httpRequest }).run({
+ coordinates: [
+ { longitude: -74.006, latitude: 40.7128 },
+ { longitude: -71.0589, latitude: 42.3601 }
+ ],
+ exclude: 'toll'
+ });
+
+ const calledUrl = mockHttpRequest.mock.calls[0][0];
+ // Should use the provided exclude parameter
+ expect(calledUrl).toContain('exclude=toll');
+ // Should not modify alternatives (defaults to false)
+ expect(calledUrl).toContain('alternatives=false');
+ });
+
+ it('does not elicit when more than 2 coordinates provided', async () => {
+ const { httpRequest, mockHttpRequest } = setupHttpRequest();
+
+ await new DirectionsTool({ httpRequest }).run({
+ coordinates: [
+ { longitude: -74.006, latitude: 40.7128 },
+ { longitude: -73.9, latitude: 41.0 },
+ { longitude: -71.0589, latitude: 42.3601 }
+ ]
+ });
+
+ const calledUrl = mockHttpRequest.mock.calls[0][0];
+ // Should not set alternatives=true (elicitation skipped for multi-waypoint)
+ expect(calledUrl).toContain('alternatives=false');
+ });
+
+ it('returns multiple routes when alternatives=true and no elicitation', async () => {
+ const mockResponse = {
+ code: 'Ok',
+ routes: [
+ {
+ duration: 17280,
+ distance: 366800,
+ legs: [
+ {
+ summary: 'I-84 East, I-90 East',
+ duration: 17280,
+ distance: 366800,
+ steps: []
+ }
+ ],
+ leg_summaries: ['I-84 East, I-90 East'],
+ congestion_information: {
+ length_low: 300000,
+ length_moderate: 50000,
+ length_heavy: 16800,
+ length_severe: 0
+ },
+ incidents_summary: [{ type: 'construction' }]
+ },
+ {
+ duration: 17880,
+ distance: 348000,
+ legs: [
+ {
+ summary: 'CT-15 North, I-90 East',
+ duration: 17880,
+ distance: 348000,
+ steps: []
+ }
+ ],
+ leg_summaries: ['CT-15 North, I-90 East'],
+ congestion_information: {
+ length_low: 280000,
+ length_moderate: 60000,
+ length_heavy: 8000,
+ length_severe: 0
+ },
+ incidents_summary: [
+ { type: 'construction' },
+ { type: 'accident' },
+ { type: 'construction' },
+ { type: 'construction' },
+ { type: 'construction' }
+ ]
+ }
+ ],
+ waypoints: []
+ };
+
+ const { httpRequest } = setupHttpRequest({
+ json: async () => mockResponse
+ });
+
+ const result = await new DirectionsTool({ httpRequest }).run({
+ coordinates: [
+ { longitude: -74.006, latitude: 40.7128 },
+ { longitude: -71.0589, latitude: 42.3601 }
+ ],
+ alternatives: true
+ });
+
+ expect(result.isError).toBe(false);
+ // Without elicitation, should return all routes
+ const response = JSON.parse(
+ (result.content[0] as { type: 'text'; text: string }).text
+ );
+ expect(response.routes).toHaveLength(2);
+ });
+
+ it('returns single route when only one route available (no Stage 2 elicitation)', async () => {
+ const mockResponse = {
+ code: 'Ok',
+ routes: [
+ {
+ duration: 17280,
+ distance: 366800,
+ legs: [
+ {
+ summary: 'I-84 East, I-90 East',
+ duration: 17280,
+ distance: 366800,
+ steps: []
+ }
+ ],
+ leg_summaries: ['I-84 East, I-90 East']
+ }
+ ],
+ waypoints: []
+ };
+
+ const { httpRequest } = setupHttpRequest({
+ json: async () => mockResponse
+ });
+
+ const result = await new DirectionsTool({ httpRequest }).run({
+ coordinates: [
+ { longitude: -74.006, latitude: 40.7128 },
+ { longitude: -71.0589, latitude: 42.3601 }
+ ],
+ alternatives: true
+ });
+
+ expect(result.isError).toBe(false);
+ // Should return the single route
+ const response = JSON.parse(
+ (result.content[0] as { type: 'text'; text: string }).text
+ );
+ expect(response.routes).toHaveLength(1);
+ });
+ });
});