From 1e72d21a350609109de0f998187a851cef976db7 Mon Sep 17 00:00:00 2001 From: Matthew Podwysocki Date: Mon, 12 Jan 2026 16:18:47 -0500 Subject: [PATCH 1/5] 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 00000000..a0cbd94d --- /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 00000000..8d62aef0 --- /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 50d53a7e..c4313fee 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/5] 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 87d6481d..1fae1526 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 5c5e77a5..304046d7 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 188f51afa854374f66571e8d34809613836d4a45 Mon Sep 17 00:00:00 2001 From: Matthew Podwysocki Date: Wed, 1 Apr 2026 14:16:36 -0400 Subject: [PATCH 3/5] chore: add CVE-2026-4926 entry to CHANGELOG Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 565eb52e..8d2c3eb5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ### Security +- **CVE-2026-4926**: Upgraded `@modelcontextprotocol/sdk` to `^1.29.0`, resolving `path-to-regexp` to `8.4.1` and fixing the ReDoS vulnerability [GHSA-j3q9-mxjg-w52f](https://github.com/advisories/GHSA-j3q9-mxjg-w52f); regenerated output-validation patch for the new version - **static_map_image_tool**: Validate `style` parameter against `username/style-id` format to prevent path traversal attacks where a crafted style value (e.g., `../../tokens/v2`) could escape the `/styles/v1/` URL path and access arbitrary Mapbox API endpoints using the server operator's token - **static_map_image_tool**: Remove access token from URL returned in text content — the token is only used internally for the HTTP fetch and the MCP Apps iframe URL, not exposed to the model context From e7a7ec5d4d7983cbdf5d833557192ec7d5d7b201 Mon Sep 17 00:00:00 2001 From: Matthew Podwysocki Date: Wed, 10 Jun 2026 13:00:47 -0400 Subject: [PATCH 4/5] feat(ground-location-tool): stream results via MCP tasks extension MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Converts ground_location_tool to registerToolTask so tool/call returns a task handle immediately rather than blocking on sampling + API fan-out. Reverse geocode and sampling classification now run in parallel in the background; POI search and isochrone kick off once the strategy resolves. Requires taskSupport:'required' — only task-capable clients can invoke this path. The server is configured with InMemoryTaskStore. Fallback for non-task clients and persistent task storage are left for a follow-up. Closes #197. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- CHANGELOG.md | 4 + src/index.ts | 4 +- src/tools/MapboxApiBasedTool.ts | 2 +- .../GroundLocationTool.ts | 162 ++++++++++++++++++ .../GroundLocationTool.test.ts | 102 +++++++++++ 5 files changed, 272 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c687321..826af157 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## Unreleased +### New Features + +- **`ground_location_tool` task-based streaming** (experimental): Convert `ground_location_tool` to use the MCP tasks extension (`server.experimental.tasks.registerToolTask`). The tool now returns a task handle immediately on `tools/call` instead of blocking until all API calls complete. Reverse geocoding and sampling classification run in parallel in the background; POI search and isochrone follow once the strategy is known. Requires a task-capable client (`taskSupport: 'required'`). The server is configured with `InMemoryTaskStore` to support task lifecycle management. See issue #197. + ### Security - **static_map_image_tool**: Stop embedding the Mapbox access token in tool results. Previously the tool returned a `createUIResource({ iframeUrl })` whose URL carried the caller's `?access_token=` query param, leaking the secret token via the MCP-UI resource item. The credentialed URL is now only used server-side to fetch the image, which is returned inline as base64. The tool's `meta.ui.resourceUri` declaration is removed (the iframe path required the credentialed URL to function and cannot be reinstated without leaking). A regression test asserts the access token does not appear in any content item. diff --git a/src/index.ts b/src/index.ts index d0f3dc65..fd1d0ee7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,6 +10,7 @@ import { existsSync } from 'node:fs'; import { SpanStatusCode } from '@opentelemetry/api'; import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { InMemoryTaskStore } from '@modelcontextprotocol/sdk/experimental/tasks'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { registerAppResource, @@ -107,7 +108,8 @@ const server = new McpServer( resources: {}, prompts: {}, logging: {} - } + }, + taskStore: new InMemoryTaskStore() } ); diff --git a/src/tools/MapboxApiBasedTool.ts b/src/tools/MapboxApiBasedTool.ts index 4febfcc0..4f0353a7 100644 --- a/src/tools/MapboxApiBasedTool.ts +++ b/src/tools/MapboxApiBasedTool.ts @@ -46,7 +46,7 @@ export abstract class MapboxApiBasedTool< * @param token The token string to validate * @returns boolean indicating if the token has valid JWT format */ - private isValidJwtFormat(token: string): boolean { + protected isValidJwtFormat(token: string): boolean { // JWT consists of three parts separated by dots: header.payload.signature const parts = token.split('.'); if (parts.length !== 3) return false; diff --git a/src/tools/ground-location-tool/GroundLocationTool.ts b/src/tools/ground-location-tool/GroundLocationTool.ts index 86f5bfa8..6c99a68e 100644 --- a/src/tools/ground-location-tool/GroundLocationTool.ts +++ b/src/tools/ground-location-tool/GroundLocationTool.ts @@ -4,6 +4,11 @@ import type { z } from 'zod'; import { MapboxApiBasedTool } from '../MapboxApiBasedTool.js'; import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; +import type { + McpServer, + RegisteredTool +} from '@modelcontextprotocol/sdk/server/mcp.js'; +import type { RequestTaskStore } from '@modelcontextprotocol/sdk/shared/protocol.js'; import type { HttpRequest } from '../../utils/types.js'; import { GroundLocationInputSchema } from './GroundLocationTool.input.schema.js'; import { @@ -88,6 +93,163 @@ export class GroundLocationTool extends MapboxApiBasedTool< }); } + override installTo(server: McpServer): RegisteredTool { + this.server = server; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const inputShape = (this.inputSchema as unknown as { shape: any }).shape; + return server.experimental.tasks.registerToolTask( + this.name, + { + title: this.annotations.title, + description: this.description, + inputSchema: inputShape, + outputSchema: this.outputSchema, + annotations: this.annotations, + execution: { taskSupport: 'required' } + }, + { + createTask: async ( + args: z.infer, + extra + ) => { + const accessToken = + extra.authInfo?.token || MapboxApiBasedTool.mapboxAccessToken; + if (!accessToken || !this.isValidJwtFormat(accessToken)) { + throw new Error( + 'No valid access token. Provide via Bearer auth or MAPBOX_ACCESS_TOKEN env var.' + ); + } + const task = await extra.taskStore.createTask({ ttl: 60_000 }); + void this.runTaskBackground( + args, + accessToken, + task.taskId, + extra.taskStore + ); + return { task }; + }, + getTask: async (_args, extra) => { + return extra.taskStore.getTask(extra.taskId); + }, + getTaskResult: async (_args, extra) => { + return extra.taskStore.getTaskResult( + extra.taskId + ) as Promise; + } + } + ); + } + + private async runTaskBackground( + rawArgs: z.infer, + accessToken: string, + taskId: string, + taskStore: RequestTaskStore + ): Promise { + try { + const { + longitude, + latitude, + query, + profile, + contours_minutes, + limit, + language + } = GroundLocationInputSchema.parse(rawArgs); + const citations: string[] = ['Mapbox Geocoding API']; + + // Kick off sampling + a fast initial geocode in parallel so the place name + // is available as soon as possible regardless of sampling latency. + const [strategy, initialGeocode] = await Promise.all([ + this.classifyGroundingStrategy(query, longitude, latitude), + this.reverseGeocode( + longitude, + latitude, + accessToken, + 'neighborhood,locality,place', + language + ) + ]); + + // Refine geocode types now that we know the strategy. + const geocodeTypes = + strategy === 'routing' + ? 'address,poi' + : strategy === 'region' + ? 'region,district,place' + : 'neighborhood,locality,place'; + + const geocodeResult = + geocodeTypes !== 'neighborhood,locality,place' + ? await this.reverseGeocode( + longitude, + latitude, + accessToken, + geocodeTypes, + language + ) + : initialGeocode; + + // Fan out POIs + isochrone now that strategy is known. + const [poisResult, isochroneResult] = await Promise.all([ + query || strategy === 'poi' + ? this.categorySearch( + query ?? 'place', + longitude, + latitude, + strategy === 'poi' ? Math.max(limit, 15) : limit, + accessToken, + language + ).then((pois) => { + if (pois?.length) citations.push('Mapbox Search API'); + return pois; + }) + : Promise.resolve(undefined), + strategy === 'region' || strategy === 'neighborhood' + ? this.isochrone( + longitude, + latitude, + profile, + contours_minutes, + accessToken + ).then((iso) => { + if (iso) citations.push('Mapbox Isochrone API'); + return iso; + }) + : Promise.resolve(undefined) + ]); + + const result: GroundLocationOutput = { + place: geocodeResult.place, + full_address: geocodeResult.full_address, + longitude, + latitude, + nearby_pois: poisResult ?? undefined, + isochrone: isochroneResult ?? undefined, + citations + }; + + const validated = GroundLocationOutputSchema.safeParse(result); + const output = validated.success ? validated.data : result; + + await taskStore.storeTaskResult(taskId, 'completed', { + content: [{ type: 'text', text: this.formatOutput(output, strategy) }], + structuredContent: output as unknown as Record, + isError: false + }); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + try { + await taskStore.storeTaskResult(taskId, 'failed', { + content: [{ type: 'text', text: message }], + isError: true + }); + } catch { + // Task may have been cancelled before we could store the failure. + } + } + } + /** * Use sampling to classify what kind of grounding the query needs. * Falls back to 'neighborhood' if sampling is unavailable or classification fails. diff --git a/test/tools/ground-location-tool/GroundLocationTool.test.ts b/test/tools/ground-location-tool/GroundLocationTool.test.ts index 9ccf192a..ae9d5092 100644 --- a/test/tools/ground-location-tool/GroundLocationTool.test.ts +++ b/test/tools/ground-location-tool/GroundLocationTool.test.ts @@ -7,6 +7,7 @@ process.env.MAPBOX_ACCESS_TOKEN = import { describe, it, expect, afterEach, vi } from 'vitest'; import { setupHttpRequest } from '../../utils/httpPipelineUtils.js'; import { GroundLocationTool } from '../../../src/tools/ground-location-tool/GroundLocationTool.js'; +import { InMemoryTaskStore } from '@modelcontextprotocol/sdk/experimental/tasks'; const geocodeResponse = { features: [ @@ -268,3 +269,104 @@ describe('GroundLocationTool', () => { expect(categoryCall?.[0]).toContain('limit=15'); }); }); + +describe('GroundLocationTool — task-based flow', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + function buildTaskStore() { + const store = new InMemoryTaskStore(); + const requestId = 1; + const request = { + method: 'tools/call', + params: { name: 'ground_location_tool', arguments: {} } + }; + // Wrap in a RequestTaskStore-compatible shim bound to a fixed session. + const taskStore = { + createTask: (params: { ttl?: number }) => + store.createTask(params, requestId, request), + getTask: (taskId: string) => + store.getTask(taskId).then((t) => { + if (!t) throw new Error(`task not found: ${taskId}`); + return t; + }), + storeTaskResult: ( + taskId: string, + status: 'completed' | 'failed', + // eslint-disable-next-line @typescript-eslint/no-explicit-any + result: any + ) => store.storeTaskResult(taskId, status, result), + getTaskResult: (taskId: string) => store.getTaskResult(taskId), + updateTaskStatus: ( + taskId: string, + status: Parameters[1] + ) => store.updateTaskStatus(taskId, status), + listTasks: (cursor?: string) => store.listTasks(cursor) + }; + return taskStore; + } + + it('creates task immediately and resolves with place name', async () => { + const { httpRequest } = setupHttpRequest(); + const mockFetch = vi.fn().mockImplementation((url: string) => { + if (url.includes('geocode/v6/reverse')) + return Promise.resolve({ + ok: true, + json: async () => geocodeResponse + }); + if (url.includes('isochrone/v1')) + return Promise.resolve({ + ok: true, + json: async () => isochroneResponse + }); + return Promise.resolve({ ok: false, json: async () => ({}) }); + }); + const tool = new GroundLocationTool({ + httpRequest: mockFetch as unknown as typeof httpRequest + }); + + const taskStore = buildTaskStore(); + const task = await taskStore.createTask({ ttl: 60_000 }); + + // Simulate what createTask handler does + // eslint-disable-next-line @typescript-eslint/no-explicit-any + await (tool as any).runTaskBackground( + { longitude: -122.419, latitude: 37.759 }, + process.env.MAPBOX_ACCESS_TOKEN, + task.taskId, + taskStore + ); + + const completedTask = await taskStore.getTask(task.taskId); + expect(completedTask.status).toBe('completed'); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = (await taskStore.getTaskResult(task.taskId)) as any; + expect(result.isError).toBe(false); + const text = result.content[0].text as string; + expect(text).toContain('Mission District'); + }); + + it('stores failed result when API errors out', async () => { + const { httpRequest } = setupHttpRequest(); + const mockFetch = vi.fn().mockRejectedValue(new Error('network error')); + const tool = new GroundLocationTool({ + httpRequest: mockFetch as unknown as typeof httpRequest + }); + + const taskStore = buildTaskStore(); + const task = await taskStore.createTask({ ttl: 60_000 }); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + await (tool as any).runTaskBackground( + { longitude: -122.419, latitude: 37.759 }, + process.env.MAPBOX_ACCESS_TOKEN, + task.taskId, + taskStore + ); + + const completedTask = await taskStore.getTask(task.taskId); + expect(completedTask.status).toBe('failed'); + }); +}); From 1d86c3ddc333c4d9b0693adaa5e445b3bf36721e Mon Sep 17 00:00:00 2001 From: Matthew Podwysocki Date: Thu, 11 Jun 2026 11:08:34 -0400 Subject: [PATCH 5/5] fix(ground-location-tool): support non-task clients via taskSupport optional Changes taskSupport from 'required' to 'optional' so clients without task support get the same synchronous result as before. The SDK automatic polling path handles the fallback: it creates the task internally, waits for the background work to finish, and returns the result directly without exposing the task handle to the caller. pollInterval is set to 50ms so the polling overhead for non-task clients is negligible. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- CHANGELOG.md | 2 +- .../GroundLocationTool.ts | 9 +++-- .../GroundLocationTool.test.ts | 36 +++++++++++++++++++ 3 files changed, 44 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 826af157..b0f23e2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ### New Features -- **`ground_location_tool` task-based streaming** (experimental): Convert `ground_location_tool` to use the MCP tasks extension (`server.experimental.tasks.registerToolTask`). The tool now returns a task handle immediately on `tools/call` instead of blocking until all API calls complete. Reverse geocoding and sampling classification run in parallel in the background; POI search and isochrone follow once the strategy is known. Requires a task-capable client (`taskSupport: 'required'`). The server is configured with `InMemoryTaskStore` to support task lifecycle management. See issue #197. +- **`ground_location_tool` task-based streaming** (experimental): Convert `ground_location_tool` to use the MCP tasks extension (`server.experimental.tasks.registerToolTask`). The tool now returns a task handle immediately on `tools/call` instead of blocking until all API calls complete. Reverse geocoding and sampling classification run in parallel in the background; POI search and isochrone follow once the strategy is known. Task-capable clients get streaming updates; clients without task support get the same synchronous result as before via the SDK automatic polling path (`taskSupport: 'optional'`). The server is configured with `InMemoryTaskStore` to support task lifecycle management. See issue #197. ### Security diff --git a/src/tools/ground-location-tool/GroundLocationTool.ts b/src/tools/ground-location-tool/GroundLocationTool.ts index 6c99a68e..eeeb0cb1 100644 --- a/src/tools/ground-location-tool/GroundLocationTool.ts +++ b/src/tools/ground-location-tool/GroundLocationTool.ts @@ -105,7 +105,7 @@ export class GroundLocationTool extends MapboxApiBasedTool< inputSchema: inputShape, outputSchema: this.outputSchema, annotations: this.annotations, - execution: { taskSupport: 'required' } + execution: { taskSupport: 'optional' } }, { createTask: async ( @@ -119,7 +119,12 @@ export class GroundLocationTool extends MapboxApiBasedTool< 'No valid access token. Provide via Bearer auth or MAPBOX_ACCESS_TOKEN env var.' ); } - const task = await extra.taskStore.createTask({ ttl: 60_000 }); + // pollInterval is set low so the SDK automatic polling path (used for + // clients that do not support tasks) has minimal extra latency. + const task = await extra.taskStore.createTask({ + ttl: 60_000, + pollInterval: 50 + }); void this.runTaskBackground( args, accessToken, diff --git a/test/tools/ground-location-tool/GroundLocationTool.test.ts b/test/tools/ground-location-tool/GroundLocationTool.test.ts index ae9d5092..11a81c4b 100644 --- a/test/tools/ground-location-tool/GroundLocationTool.test.ts +++ b/test/tools/ground-location-tool/GroundLocationTool.test.ts @@ -369,4 +369,40 @@ describe('GroundLocationTool — task-based flow', () => { const completedTask = await taskStore.getTask(task.taskId); expect(completedTask.status).toBe('failed'); }); + + it('non-task clients still get a result via runTaskBackground', async () => { + const { httpRequest } = setupHttpRequest(); + const mockFetch = vi.fn().mockImplementation((url: string) => { + if (url.includes('geocode/v6/reverse')) + return Promise.resolve({ + ok: true, + json: async () => geocodeResponse + }); + if (url.includes('isochrone/v1')) + return Promise.resolve({ + ok: true, + json: async () => isochroneResponse + }); + return Promise.resolve({ ok: false, json: async () => ({}) }); + }); + const tool = new GroundLocationTool({ + httpRequest: mockFetch as unknown as typeof httpRequest + }); + + const taskStore = buildTaskStore(); + const task = await taskStore.createTask({ ttl: 60_000, pollInterval: 50 }); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + await (tool as any).runTaskBackground( + { longitude: -122.419, latitude: 37.759 }, + process.env.MAPBOX_ACCESS_TOKEN, + task.taskId, + taskStore + ); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = (await taskStore.getTaskResult(task.taskId)) as any; + expect(result.isError).toBe(false); + expect(result.content[0].text).toContain('Mission District'); + }); });