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/search-and-geocode-tool/SearchAndGeocodeTool.ts b/src/tools/search-and-geocode-tool/SearchAndGeocodeTool.ts
index ebabf3b..87948b2 100644
--- a/src/tools/search-and-geocode-tool/SearchAndGeocodeTool.ts
+++ b/src/tools/search-and-geocode-tool/SearchAndGeocodeTool.ts
@@ -211,6 +211,89 @@ export class SearchAndGeocodeTool extends MapboxApiBasedTool<
`SearchAndGeocodeTool: Successfully completed search, found ${data.features?.length || 0} results`
);
+ // Check if we have multiple results that might be ambiguous
+ if (
+ this.server &&
+ data.features &&
+ data.features.length >= 2 &&
+ data.features.length <= 10
+ ) {
+ // Use elicitation to let user choose which result they want
+ try {
+ const options = data.features.map((feature, index) => {
+ const props = feature.properties || {};
+ let label = props.name || 'Unknown';
+ if (props.place_formatted) {
+ label += ` - ${props.place_formatted}`;
+ } else if (props.full_address) {
+ label += ` - ${props.full_address}`;
+ }
+ return { value: String(index), label };
+ });
+
+ // Create a JSON Schema with enum for the selection
+ const result = await this.server.server.elicitInput({
+ mode: 'form',
+ message: `Found ${data.features.length} results for "${input.q}". Please select the correct location:`,
+ requestedSchema: {
+ type: 'object',
+ properties: {
+ selectedIndex: {
+ type: 'string',
+ title: 'Select Location',
+ description: 'Choose the correct location from the results',
+ enum: options.map((o) => o.value),
+ // Include labels for better UX (some clients may support this)
+ enumNames: options.map((o) => o.label)
+ }
+ },
+ required: ['selectedIndex']
+ }
+ });
+
+ if (result.action === 'accept' && result.content?.selectedIndex) {
+ const selectedIndexStr =
+ typeof result.content.selectedIndex === 'string'
+ ? result.content.selectedIndex
+ : String(result.content.selectedIndex);
+ const selectedIndex = parseInt(selectedIndexStr, 10);
+ const selectedFeature = data.features[selectedIndex];
+
+ // Return only the selected result
+ const singleResult: SearchBoxResponse = {
+ ...data,
+ features: [selectedFeature]
+ };
+
+ return {
+ content: [
+ {
+ type: 'text',
+ text: this.formatGeoJsonToText(
+ singleResult as MapboxFeatureCollection
+ )
+ }
+ ],
+ structuredContent: singleResult,
+ isError: false
+ };
+ } else if (result.action === 'decline') {
+ // User declined to select - return all results as before
+ this.log(
+ 'info',
+ 'SearchAndGeocodeTool: User declined to select a specific result'
+ );
+ }
+ } catch (elicitError) {
+ // If elicitation fails, fall back to returning all results
+ this.log(
+ 'warning',
+ `SearchAndGeocodeTool: Elicitation failed: ${elicitError instanceof Error ? elicitError.message : 'Unknown error'}`
+ );
+ }
+ }
+
+ // Default behavior: return all results
return {
content: [
{
diff --git a/test/tools/search-and-geocode-tool/SearchAndGeocodeTool.test.ts b/test/tools/search-and-geocode-tool/SearchAndGeocodeTool.test.ts
index 9bda04e..5d8ee20 100644
--- a/test/tools/search-and-geocode-tool/SearchAndGeocodeTool.test.ts
+++ b/test/tools/search-and-geocode-tool/SearchAndGeocodeTool.test.ts
@@ -391,4 +391,230 @@ describe('SearchAndGeocodeTool', () => {
isError: true
});
});
+
+ describe('Elicitation behavior', () => {
+ const createMockServer = (elicitResponse?: {
+ action: 'accept' | 'decline';
+ content?: Record;
+ }) => {
+ return {
+ server: {
+ elicitInput: vi.fn().mockResolvedValue(
+ elicitResponse || {
+ action: 'accept',
+ content: { selectedIndex: '0' }
+ }
+ ),
+ sendLoggingMessage: vi.fn()
+ },
+ registerTool: vi.fn()
+ } as any;
+ };
+
+ const createMultipleResultsResponse = (count: number) => ({
+ type: 'FeatureCollection',
+ features: Array.from({ length: count }, (_, i) => ({
+ type: 'Feature',
+ properties: {
+ name: `Springfield #${i + 1}`,
+ place_formatted: `Springfield, State ${i + 1}, United States`
+ },
+ geometry: {
+ type: 'Point',
+ coordinates: [-73.0 - i, 42.0 + i]
+ }
+ }))
+ });
+
+ it('triggers elicitation when 2-10 results returned', async () => {
+ const mockResponse = createMultipleResultsResponse(5);
+ const { httpRequest } = setupHttpRequest({
+ json: async () => mockResponse
+ });
+
+ const tool = new SearchAndGeocodeTool({ httpRequest });
+ const mockServer = createMockServer();
+ tool.installTo(mockServer);
+
+ await tool.run({ q: 'Springfield' });
+
+ expect(mockServer.server.elicitInput).toHaveBeenCalledOnce();
+ expect(mockServer.server.elicitInput).toHaveBeenCalledWith({
+ mode: 'form',
+ message:
+ 'Found 5 results for "Springfield". Please select the correct location:',
+ requestedSchema: expect.objectContaining({
+ type: 'object',
+ properties: expect.objectContaining({
+ selectedIndex: expect.objectContaining({
+ type: 'string',
+ title: 'Select Location',
+ enum: ['0', '1', '2', '3', '4'],
+ enumNames: expect.arrayContaining([
+ expect.stringContaining('Springfield #1'),
+ expect.stringContaining('Springfield #2')
+ ])
+ })
+ }),
+ required: ['selectedIndex']
+ })
+ });
+ });
+
+ it('does not trigger elicitation with only 1 result', async () => {
+ const mockResponse = createMultipleResultsResponse(1);
+ const { httpRequest } = setupHttpRequest({
+ json: async () => mockResponse
+ });
+
+ const tool = new SearchAndGeocodeTool({ httpRequest });
+ const mockServer = createMockServer();
+ tool.installTo(mockServer);
+
+ const result = await tool.run({ q: 'Paris' });
+
+ expect(mockServer.server.elicitInput).not.toHaveBeenCalled();
+ expect(result.isError).toBe(false);
+ expect((result.structuredContent as any).features).toHaveLength(1);
+ });
+
+ it('does not trigger elicitation with more than 10 results', async () => {
+ const mockResponse = createMultipleResultsResponse(15);
+ const { httpRequest } = setupHttpRequest({
+ json: async () => mockResponse
+ });
+
+ const tool = new SearchAndGeocodeTool({ httpRequest });
+ const mockServer = createMockServer();
+ tool.installTo(mockServer);
+
+ const result = await tool.run({ q: 'Main Street' });
+
+ expect(mockServer.server.elicitInput).not.toHaveBeenCalled();
+ expect(result.isError).toBe(false);
+ expect((result.structuredContent as any).features).toHaveLength(15);
+ });
+
+ it('returns only selected result when user accepts elicitation', async () => {
+ const mockResponse = createMultipleResultsResponse(3);
+ const { httpRequest } = setupHttpRequest({
+ json: async () => mockResponse
+ });
+
+ const tool = new SearchAndGeocodeTool({ httpRequest });
+ const mockServer = createMockServer({
+ action: 'accept',
+ content: { selectedIndex: '1' } // Select second item
+ });
+ tool.installTo(mockServer);
+
+ const result = await tool.run({ q: 'Springfield' });
+
+ expect(result.isError).toBe(false);
+ const features = (result.structuredContent as any).features;
+ expect(features).toHaveLength(1);
+ expect(features[0].properties.name).toBe('Springfield #2');
+ });
+
+ it('returns all results when user declines elicitation', async () => {
+ const mockResponse = createMultipleResultsResponse(4);
+ const { httpRequest } = setupHttpRequest({
+ json: async () => mockResponse
+ });
+
+ const tool = new SearchAndGeocodeTool({ httpRequest });
+ const mockServer = createMockServer({
+ action: 'decline'
+ });
+ tool.installTo(mockServer);
+
+ const result = await tool.run({ q: 'Springfield' });
+
+ expect(result.isError).toBe(false);
+ const features = (result.structuredContent as any).features;
+ expect(features).toHaveLength(4);
+ });
+
+ it('falls back to all results when elicitation fails', async () => {
+ const mockResponse = createMultipleResultsResponse(3);
+ const { httpRequest } = setupHttpRequest({
+ json: async () => mockResponse
+ });
+
+ const tool = new SearchAndGeocodeTool({ httpRequest });
+ const mockServer = {
+ server: {
+ elicitInput: vi
+ .fn()
+ .mockRejectedValue(new Error('Elicitation not supported')),
+ sendLoggingMessage: vi.fn()
+ },
+ registerTool: vi.fn()
+ } as any;
+ tool.installTo(mockServer);
+
+ const result = await tool.run({ q: 'Springfield' });
+
+ expect(result.isError).toBe(false);
+ const features = (result.structuredContent as any).features;
+ expect(features).toHaveLength(3);
+ });
+
+ it('handles elicitation gracefully when server is not installed', async () => {
+ const mockResponse = createMultipleResultsResponse(5);
+ const { httpRequest } = setupHttpRequest({
+ json: async () => mockResponse
+ });
+
+ const tool = new SearchAndGeocodeTool({ httpRequest });
+ // Don't install to server - tool.server will be null
+
+ const result = await tool.run({ q: 'Springfield' });
+
+ expect(result.isError).toBe(false);
+ const features = (result.structuredContent as any).features;
+ expect(features).toHaveLength(5);
+ });
+
+ it('builds correct enumNames with location labels', async () => {
+ const mockResponse = {
+ type: 'FeatureCollection',
+ features: [
+ {
+ type: 'Feature',
+ properties: {
+ name: 'Springfield',
+ place_formatted: 'Springfield, Illinois, United States'
+ },
+ geometry: { type: 'Point', coordinates: [-89.6501, 39.7817] }
+ },
+ {
+ type: 'Feature',
+ properties: {
+ name: 'Springfield',
+ full_address: '123 Main St, Springfield, MA 01103'
+ },
+ geometry: { type: 'Point', coordinates: [-72.5301, 42.1015] }
+ }
+ ]
+ };
+ const { httpRequest } = setupHttpRequest({
+ json: async () => mockResponse
+ });
+
+ const tool = new SearchAndGeocodeTool({ httpRequest });
+ const mockServer = createMockServer();
+ tool.installTo(mockServer);
+
+ await tool.run({ q: 'Springfield' });
+
+ const elicitCall = mockServer.server.elicitInput.mock.calls[0][0];
+ expect(
+ elicitCall.requestedSchema.properties.selectedIndex.enumNames
+ ).toEqual([
+ 'Springfield - Springfield, Illinois, United States',
+ 'Springfield - 123 Main St, Springfield, MA 01103'
+ ]);
+ });
+ });
});