From 1e72d21a350609109de0f998187a851cef976db7 Mon Sep 17 00:00:00 2001 From: Matthew Podwysocki Date: Mon, 12 Jan 2026 16:18:47 -0500 Subject: [PATCH 1/4] Add server-level icons with light and dark theme support Implements MCP server icons at the correct architectural level (server initialization) instead of at the tool level. Adds both light and dark theme variants of the Mapbox logo using base64-encoded SVG data URIs. - Add mapbox-logo-black.svg for light theme backgrounds - Add mapbox-logo-white.svg for dark theme backgrounds - Update server initialization to include icons array with theme property - Use 800x180 SVG logos embedded as base64 data URIs This replaces the previous incorrect approach of adding icons to individual tools, which was not aligned with the MCP specification. Co-Authored-By: Claude Sonnet 4.5 --- assets/mapbox-logo-black.svg | 38 ++++++++++++++++++++++++++++++++ assets/mapbox-logo-white.svg | 42 ++++++++++++++++++++++++++++++++++++ src/index.ts | 16 +++++++++++++- 3 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 assets/mapbox-logo-black.svg create mode 100644 assets/mapbox-logo-white.svg 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 @@ + + + +Mapbox_Logo_08 + + + + + + + + + + + + + + + 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 @@ + + + + +Mapbox_Logo_08 + + + + + + + + + + + + + + + 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: { From c20582133bba072c7d879d77d1650a06c301bc07 Mon Sep 17 00:00:00 2001 From: Matthew Podwysocki Date: Mon, 12 Jan 2026 16:24:43 -0500 Subject: [PATCH 2/4] Update @modelcontextprotocol/sdk to 1.25.2 Updates the MCP SDK from 1.25.1 to 1.25.2 and recreates the output validation patch for the new version. The patch continues to convert strict output schema validation errors to warnings, allowing tools to gracefully handle schema mismatches. Changes: - Update @modelcontextprotocol/sdk from ^1.25.1 to ^1.25.2 - Recreate SDK patch for version 1.25.2 - Remove obsolete 1.25.1 patch file - All 397 tests pass with new SDK version Co-Authored-By: Claude Sonnet 4.5 --- package-lock.json | 8 ++++---- package.json | 2 +- ....25.1.patch => @modelcontextprotocol+sdk+1.25.2.patch} | 0 3 files changed, 5 insertions(+), 5 deletions(-) rename patches/{@modelcontextprotocol+sdk+1.25.1.patch => @modelcontextprotocol+sdk+1.25.2.patch} (100%) diff --git a/package-lock.json b/package-lock.json index 87d6481..1fae152 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "license": "MIT", "dependencies": { "@mcp-ui/server": "^5.13.1", - "@modelcontextprotocol/sdk": "^1.25.1", + "@modelcontextprotocol/sdk": "^1.25.2", "@opentelemetry/api": "^1.9.0", "@opentelemetry/auto-instrumentations-node": "^0.56.0", "@opentelemetry/exporter-trace-otlp-http": "^0.56.0", @@ -1913,9 +1913,9 @@ } }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.25.1.tgz", - "integrity": "sha512-yO28oVFFC7EBoiKdAn+VqRm+plcfv4v0xp6osG/VsCB0NlPZWi87ajbCZZ8f/RvOFLEu7//rSRmuZZ7lMoe3gQ==", + "version": "1.25.2", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.25.2.tgz", + "integrity": "sha512-LZFeo4F9M5qOhC/Uc1aQSrBHxMrvxett+9KLHt7OhcExtoiRN9DKgbZffMP/nxjutWDQpfMDfP3nkHI4X9ijww==", "license": "MIT", "dependencies": { "@hono/node-server": "^1.19.7", diff --git a/package.json b/package.json index 5c5e77a..304046d 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,7 @@ ], "dependencies": { "@mcp-ui/server": "^5.13.1", - "@modelcontextprotocol/sdk": "^1.25.1", + "@modelcontextprotocol/sdk": "^1.25.2", "@opentelemetry/api": "^1.9.0", "@opentelemetry/auto-instrumentations-node": "^0.56.0", "@opentelemetry/exporter-trace-otlp-http": "^0.56.0", diff --git a/patches/@modelcontextprotocol+sdk+1.25.1.patch b/patches/@modelcontextprotocol+sdk+1.25.2.patch similarity index 100% rename from patches/@modelcontextprotocol+sdk+1.25.1.patch rename to patches/@modelcontextprotocol+sdk+1.25.2.patch From 1a715d7009253e4fafefd5f35c73e5e9ac5562e2 Mon Sep 17 00:00:00 2001 From: Matthew Podwysocki Date: Wed, 14 Jan 2026 16:18:16 -0500 Subject: [PATCH 3/4] Add two-stage elicitations for DirectionsTool MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements a two-stage elicitation flow for better route selection: Stage 1 (Routing Preferences): - Elicits user preferences before API call (tolls, highways, ferries) - Maps preferences to exclude parameter - Only triggers for simple A-to-B routes (2 coordinates) - Forces alternatives=true to get multiple routes for Stage 2 Stage 2 (Route Selection): - Presents actual routes with formatted details after API call - Shows duration, distance, traffic levels (✅🟡⚠️), and incident counts - User selects preferred route from 2+ alternatives - Returns only the selected route Helper methods: - formatDuration: Convert seconds to "4h 48min" - formatDistance: Convert meters to "366.8km (227.9mi)" Graceful fallback: - Both stages catch errors and continue - Works without elicitation support (returns all routes) Co-Authored-By: Claude Sonnet 4.5 --- src/tools/directions-tool/DirectionsTool.ts | 217 ++++++++++++++++++ .../directions-tool/DirectionsTool.test.ts | 151 ++++++++++++ 2 files changed, 368 insertions(+) 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/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); + }); + }); }); From f24cecc5634d2d861ff28617529a3fbdfcc81652 Mon Sep 17 00:00:00 2001 From: Matthew Podwysocki Date: Thu, 15 Jan 2026 15:43:47 -0500 Subject: [PATCH 4/4] Fix schema validation for duration_typical null values Same fix as PR #100 - allows duration_typical and weight_typical to be null. This fixes the validation error when declining elicitations and getting routes with null duration_typical values from the API. --- .../directions-tool/DirectionsTool.output.schema.ts | 12 ++++++------ src/tools/directions-tool/cleanResponseData.ts | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) 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/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;