From f91469469262dc5ca6f9d6407f3ed749e807b2a9 Mon Sep 17 00:00:00 2001 From: Bernardo Donadio Date: Fri, 10 Oct 2025 02:45:07 -0300 Subject: [PATCH 1/2] (feat) Add stream-http support mcp-hub had stream-http support between itself and mcp-servers, but not between mcp-hub and clients. This commit adds this support and updates the SDK spec version. This is specially important for Codex, which DOOES NOT support SSE at all. --- CHANGELOG.md | 73 ++++++++ package-lock.json | 426 +++++++++++++++++++++++++--------------------- package.json | 18 +- src/mcp/server.js | 94 ++++++++++ src/server.js | 42 ++++- 5 files changed, 449 insertions(+), 204 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f93a73..06afea7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,79 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [5.0.2] - 2025-10-10 + +### Fixed + +- **Streamable HTTP Session Initialization**: Fixed "Transport channel closed" error during session initialization + - MCP server now connects to transport BEFORE handling the first request + - Transport instances are properly reused for subsequent requests in the same session + - Session ID lookup now works correctly for existing sessions + - Resolves race condition where transport would close before server initialization completed + +## [5.0.1] - 2025-10-10 + +### Fixed + +- **DNS Rebinding Protection**: Disabled by default to prevent 403 Forbidden errors during local development + - Streamable HTTP transport now accepts connections without strict origin/host validation + - DNS rebinding protection can be enabled manually in production environments if needed + - Resolves connection issues where legitimate localhost requests were being rejected + +## [5.0.0] - 2025-10-10 + +### Added + +- **Streamable HTTP Transport for Client Connections**: Full support for MCP 2025-03-26 specification + - Unified `/mcp` endpoint supporting both POST and GET requests + - Cryptographically secure session management with UUID-based session IDs + - DNS rebinding protection (disabled by default for ease of use, configurable for production) + - Automatic transport detection: Streamable HTTP for modern clients, SSE for legacy clients + - Seamless backward compatibility with existing SSE-based clients + - Session lifecycle callbacks for proper resource management + - Efficient bidirectional communication following latest MCP protocol + +### Changed + +- **Updated MCP SDK**: Upgraded from v1.15.1 to v1.20.0 + - Includes latest Streamable HTTP transport implementation + - Full support for MCP 2025-03-26 protocol specification + - Enhanced security features and session management + +- **Dependency Updates**: Updated all dev dependencies to latest versions + - `@eslint/js`: 9.31.0 → 9.37.0 + - `eslint`: 9.31.0 → 9.37.0 + - `esbuild`: 0.25.3 → 0.25.10 + - `globals`: 16.2.0 → 16.4.0 + - `nock`: 14.0.5 → 14.0.10 + - `open`: 10.1.2 → 10.2.0 + - `supertest`: 7.1.1 → 7.1.4 + +### Enhanced + +- **Transport Architecture**: Modernized client-hub communication layer + - Hub now supports both Streamable HTTP (primary) and SSE (fallback) for client connections + - Hub-to-server connections already supported Streamable HTTP, now client-to-hub does too + - Single unified endpoint eliminates need for separate `/mcp` and `/messages` endpoints + - Improved error handling and connection management + - Better logging with transport type identification + +- **Documentation**: Updated README to reflect Streamable HTTP support + - Clarified transport support matrix for all connection types + - Added transport auto-detection information + - Updated component descriptions for modern transport layer + - Enhanced feature table with accurate transport capabilities + +### Migration Notes + +This is a **non-breaking change** despite the major version bump: +- Existing clients using SSE transport will continue to work without modification +- New clients can use Streamable HTTP transport for improved performance +- No configuration changes required +- Automatic transport detection handles both old and new clients seamlessly + +The major version bump reflects the significant protocol upgrade and alignment with the latest MCP specification (2025-03-26). + ## [4.2.1] - 2025-08-22 ### Fixed diff --git a/package-lock.json b/package-lock.json index a6c5134..38f57c4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -110,9 +110,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.3.tgz", - "integrity": "sha512-W8bFfPA8DowP8l//sxjJLSLkD8iEjMc7cBVyP+u4cEv9sM7mdUCkgsj+t0n/BWPFtv7WWCN5Yzj0N6FJNUUqBQ==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.10.tgz", + "integrity": "sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==", "cpu": [ "ppc64" ], @@ -127,9 +127,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.3.tgz", - "integrity": "sha512-PuwVXbnP87Tcff5I9ngV0lmiSu40xw1At6i3GsU77U7cjDDB4s0X2cyFuBiDa1SBk9DnvWwnGvVaGBqoFWPb7A==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.10.tgz", + "integrity": "sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==", "cpu": [ "arm" ], @@ -144,9 +144,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.3.tgz", - "integrity": "sha512-XelR6MzjlZuBM4f5z2IQHK6LkK34Cvv6Rj2EntER3lwCBFdg6h2lKbtRjpTTsdEjD/WSe1q8UyPBXP1x3i/wYQ==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.10.tgz", + "integrity": "sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==", "cpu": [ "arm64" ], @@ -161,9 +161,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.3.tgz", - "integrity": "sha512-ogtTpYHT/g1GWS/zKM0cc/tIebFjm1F9Aw1boQ2Y0eUQ+J89d0jFY//s9ei9jVIlkYi8AfOjiixcLJSGNSOAdQ==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.10.tgz", + "integrity": "sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==", "cpu": [ "x64" ], @@ -178,9 +178,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.3.tgz", - "integrity": "sha512-eESK5yfPNTqpAmDfFWNsOhmIOaQA59tAcF/EfYvo5/QWQCzXn5iUSOnqt3ra3UdzBv073ykTtmeLJZGt3HhA+w==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.10.tgz", + "integrity": "sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==", "cpu": [ "arm64" ], @@ -195,9 +195,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.3.tgz", - "integrity": "sha512-Kd8glo7sIZtwOLcPbW0yLpKmBNWMANZhrC1r6K++uDR2zyzb6AeOYtI6udbtabmQpFaxJ8uduXMAo1gs5ozz8A==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.10.tgz", + "integrity": "sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==", "cpu": [ "x64" ], @@ -212,9 +212,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.3.tgz", - "integrity": "sha512-EJiyS70BYybOBpJth3M0KLOus0n+RRMKTYzhYhFeMwp7e/RaajXvP+BWlmEXNk6uk+KAu46j/kaQzr6au+JcIw==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.10.tgz", + "integrity": "sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==", "cpu": [ "arm64" ], @@ -229,9 +229,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.3.tgz", - "integrity": "sha512-Q+wSjaLpGxYf7zC0kL0nDlhsfuFkoN+EXrx2KSB33RhinWzejOd6AvgmP5JbkgXKmjhmpfgKZq24pneodYqE8Q==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.10.tgz", + "integrity": "sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==", "cpu": [ "x64" ], @@ -246,9 +246,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.3.tgz", - "integrity": "sha512-dUOVmAUzuHy2ZOKIHIKHCm58HKzFqd+puLaS424h6I85GlSDRZIA5ycBixb3mFgM0Jdh+ZOSB6KptX30DD8YOQ==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.10.tgz", + "integrity": "sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==", "cpu": [ "arm" ], @@ -263,9 +263,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.3.tgz", - "integrity": "sha512-xCUgnNYhRD5bb1C1nqrDV1PfkwgbswTTBRbAd8aH5PhYzikdf/ddtsYyMXFfGSsb/6t6QaPSzxtbfAZr9uox4A==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.10.tgz", + "integrity": "sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==", "cpu": [ "arm64" ], @@ -280,9 +280,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.3.tgz", - "integrity": "sha512-yplPOpczHOO4jTYKmuYuANI3WhvIPSVANGcNUeMlxH4twz/TeXuzEP41tGKNGWJjuMhotpGabeFYGAOU2ummBw==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.10.tgz", + "integrity": "sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==", "cpu": [ "ia32" ], @@ -297,9 +297,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.3.tgz", - "integrity": "sha512-P4BLP5/fjyihmXCELRGrLd793q/lBtKMQl8ARGpDxgzgIKJDRJ/u4r1A/HgpBpKpKZelGct2PGI4T+axcedf6g==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.10.tgz", + "integrity": "sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==", "cpu": [ "loong64" ], @@ -314,9 +314,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.3.tgz", - "integrity": "sha512-eRAOV2ODpu6P5divMEMa26RRqb2yUoYsuQQOuFUexUoQndm4MdpXXDBbUoKIc0iPa4aCO7gIhtnYomkn2x+bag==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.10.tgz", + "integrity": "sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==", "cpu": [ "mips64el" ], @@ -331,9 +331,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.3.tgz", - "integrity": "sha512-ZC4jV2p7VbzTlnl8nZKLcBkfzIf4Yad1SJM4ZMKYnJqZFD4rTI+pBG65u8ev4jk3/MPwY9DvGn50wi3uhdaghg==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.10.tgz", + "integrity": "sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==", "cpu": [ "ppc64" ], @@ -348,9 +348,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.3.tgz", - "integrity": "sha512-LDDODcFzNtECTrUUbVCs6j9/bDVqy7DDRsuIXJg6so+mFksgwG7ZVnTruYi5V+z3eE5y+BJZw7VvUadkbfg7QA==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.10.tgz", + "integrity": "sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==", "cpu": [ "riscv64" ], @@ -365,9 +365,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.3.tgz", - "integrity": "sha512-s+w/NOY2k0yC2p9SLen+ymflgcpRkvwwa02fqmAwhBRI3SC12uiS10edHHXlVWwfAagYSY5UpmT/zISXPMW3tQ==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.10.tgz", + "integrity": "sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==", "cpu": [ "s390x" ], @@ -382,9 +382,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.3.tgz", - "integrity": "sha512-nQHDz4pXjSDC6UfOE1Fw9Q8d6GCAd9KdvMZpfVGWSJztYCarRgSDfOVBY5xwhQXseiyxapkiSJi/5/ja8mRFFA==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.10.tgz", + "integrity": "sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==", "cpu": [ "x64" ], @@ -399,9 +399,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.3.tgz", - "integrity": "sha512-1QaLtOWq0mzK6tzzp0jRN3eccmN3hezey7mhLnzC6oNlJoUJz4nym5ZD7mDnS/LZQgkrhEbEiTn515lPeLpgWA==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.10.tgz", + "integrity": "sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==", "cpu": [ "arm64" ], @@ -416,9 +416,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.3.tgz", - "integrity": "sha512-i5Hm68HXHdgv8wkrt+10Bc50zM0/eonPb/a/OFVfB6Qvpiirco5gBA5bz7S2SHuU+Y4LWn/zehzNX14Sp4r27g==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.10.tgz", + "integrity": "sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==", "cpu": [ "x64" ], @@ -433,9 +433,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.3.tgz", - "integrity": "sha512-zGAVApJEYTbOC6H/3QBr2mq3upG/LBEXr85/pTtKiv2IXcgKV0RT0QA/hSXZqSvLEpXeIxah7LczB4lkiYhTAQ==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.10.tgz", + "integrity": "sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==", "cpu": [ "arm64" ], @@ -450,9 +450,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.3.tgz", - "integrity": "sha512-fpqctI45NnCIDKBH5AXQBsD0NDPbEFczK98hk/aa6HJxbl+UtLkJV2+Bvy5hLSLk3LHmqt0NTkKNso1A9y1a4w==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.10.tgz", + "integrity": "sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==", "cpu": [ "x64" ], @@ -466,10 +466,27 @@ "node": ">=18" } }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.10.tgz", + "integrity": "sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.3.tgz", - "integrity": "sha512-ROJhm7d8bk9dMCUZjkS8fgzsPAZEjtRJqCAmVgB0gMrvG7hfmPmz9k1rwO4jSiblFjYmNvbECL9uhaPzONMfgA==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.10.tgz", + "integrity": "sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==", "cpu": [ "x64" ], @@ -484,9 +501,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.3.tgz", - "integrity": "sha512-YWcow8peiHpNBiIXHwaswPnAXLsLVygFwCB3A7Bh5jRkIBFWHGmNQ48AlX4xDvQNoMZlPYzjVOQDYEzWCqufMQ==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.10.tgz", + "integrity": "sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==", "cpu": [ "arm64" ], @@ -501,9 +518,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.3.tgz", - "integrity": "sha512-qspTZOIGoXVS4DpNqUYUs9UxVb04khS1Degaw/MnfMe7goQ3lTfQ13Vw4qY/Nj0979BGvMRpAYbs/BAxEvU8ew==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.10.tgz", + "integrity": "sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==", "cpu": [ "ia32" ], @@ -518,9 +535,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.3.tgz", - "integrity": "sha512-ICgUR+kPimx0vvRzf+N/7L7tVSQeE3BYY+NhHRHXS1kBuPO7z2+7ea2HbhDyZdTephgvNvKrlDDKUexuCVBVvg==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.10.tgz", + "integrity": "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==", "cpu": [ "x64" ], @@ -535,9 +552,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", - "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", "dev": true, "license": "MIT", "dependencies": { @@ -592,19 +609,22 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", - "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.0.tgz", + "integrity": "sha512-WUFvV4WoIwW8Bv0KeKCIIEgdSiFOsulyN0xrMu+7z43q/hkOLXjvb5u7UC9jDxvRzcrbEmuZBX5yJZz1741jog==", "dev": true, "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.16.0" + }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/core": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", - "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.16.0.tgz", + "integrity": "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -652,9 +672,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.31.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.31.0.tgz", - "integrity": "sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==", + "version": "9.37.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.37.0.tgz", + "integrity": "sha512-jaS+NJ+hximswBG6pjNX0uEJZkrT0zwpVi3BA3vX22aFGjJjmgSTSmPpZCRKmoBL5VY/M6p0xsSJx7rk7sy5gg==", "dev": true, "license": "MIT", "engines": { @@ -675,13 +695,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.2.tgz", - "integrity": "sha512-4SaFZCNfJqvk/kenHpI8xvN42DMaoycy4PzKc5otHxRswww1kAt82OlBuwRVLofCACCTZEcla2Ydxv8scMXaTg==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.0.tgz", + "integrity": "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.15.0", + "@eslint/core": "^0.16.0", "levn": "^0.4.1" }, "engines": { @@ -836,9 +856,9 @@ } }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.15.1.tgz", - "integrity": "sha512-W/XlN9c528yYn+9MQkVjxiTPgPxoxt+oczfjHBDsJx0+59+O7B75Zhsp0B16Xbwbz8ANISDajh6+V7nIcPMc5w==", + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.20.0.tgz", + "integrity": "sha512-kOQ4+fHuT4KbR2iq2IjeV32HiihueuOf1vJkq18z08CLZ1UQrTc8BXJpVfxZkq45+inLLD+D4xx4nBjUelJa4Q==", "dev": true, "license": "MIT", "dependencies": { @@ -1099,9 +1119,9 @@ } }, "node_modules/@mswjs/interceptors": { - "version": "0.38.7", - "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.38.7.tgz", - "integrity": "sha512-Jkb27iSn7JPdkqlTqKfhncFfnEZsIJVYxsFbUSWEkxdIPdsyngrhoDBk0/BGD2FQcRH99vlRrkHpNTyKqI+0/w==", + "version": "0.39.7", + "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.39.7.tgz", + "integrity": "sha512-sURvQbbKsq5f8INV54YJgJEdk8oxBanqkTiXXd33rKmofFCwZLhLRszPduMZ9TA9b8/1CHc/IJmOlBHJk2Q5AQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1887,9 +1907,9 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -2529,9 +2549,9 @@ } }, "node_modules/esbuild": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.3.tgz", - "integrity": "sha512-qKA6Pvai73+M2FtftpNKRxJ78GIjmFXFxd/1DVBqGo/qNhLSfv+G12n9pNoWdytJC8U00TrViOwpjT0zgqQS8Q==", + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz", + "integrity": "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -2542,31 +2562,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.3", - "@esbuild/android-arm": "0.25.3", - "@esbuild/android-arm64": "0.25.3", - "@esbuild/android-x64": "0.25.3", - "@esbuild/darwin-arm64": "0.25.3", - "@esbuild/darwin-x64": "0.25.3", - "@esbuild/freebsd-arm64": "0.25.3", - "@esbuild/freebsd-x64": "0.25.3", - "@esbuild/linux-arm": "0.25.3", - "@esbuild/linux-arm64": "0.25.3", - "@esbuild/linux-ia32": "0.25.3", - "@esbuild/linux-loong64": "0.25.3", - "@esbuild/linux-mips64el": "0.25.3", - "@esbuild/linux-ppc64": "0.25.3", - "@esbuild/linux-riscv64": "0.25.3", - "@esbuild/linux-s390x": "0.25.3", - "@esbuild/linux-x64": "0.25.3", - "@esbuild/netbsd-arm64": "0.25.3", - "@esbuild/netbsd-x64": "0.25.3", - "@esbuild/openbsd-arm64": "0.25.3", - "@esbuild/openbsd-x64": "0.25.3", - "@esbuild/sunos-x64": "0.25.3", - "@esbuild/win32-arm64": "0.25.3", - "@esbuild/win32-ia32": "0.25.3", - "@esbuild/win32-x64": "0.25.3" + "@esbuild/aix-ppc64": "0.25.10", + "@esbuild/android-arm": "0.25.10", + "@esbuild/android-arm64": "0.25.10", + "@esbuild/android-x64": "0.25.10", + "@esbuild/darwin-arm64": "0.25.10", + "@esbuild/darwin-x64": "0.25.10", + "@esbuild/freebsd-arm64": "0.25.10", + "@esbuild/freebsd-x64": "0.25.10", + "@esbuild/linux-arm": "0.25.10", + "@esbuild/linux-arm64": "0.25.10", + "@esbuild/linux-ia32": "0.25.10", + "@esbuild/linux-loong64": "0.25.10", + "@esbuild/linux-mips64el": "0.25.10", + "@esbuild/linux-ppc64": "0.25.10", + "@esbuild/linux-riscv64": "0.25.10", + "@esbuild/linux-s390x": "0.25.10", + "@esbuild/linux-x64": "0.25.10", + "@esbuild/netbsd-arm64": "0.25.10", + "@esbuild/netbsd-x64": "0.25.10", + "@esbuild/openbsd-arm64": "0.25.10", + "@esbuild/openbsd-x64": "0.25.10", + "@esbuild/openharmony-arm64": "0.25.10", + "@esbuild/sunos-x64": "0.25.10", + "@esbuild/win32-arm64": "0.25.10", + "@esbuild/win32-ia32": "0.25.10", + "@esbuild/win32-x64": "0.25.10" } }, "node_modules/escalade": { @@ -2600,20 +2621,20 @@ } }, "node_modules/eslint": { - "version": "9.31.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.31.0.tgz", - "integrity": "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==", + "version": "9.37.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.37.0.tgz", + "integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", - "@eslint/config-helpers": "^0.3.0", - "@eslint/core": "^0.15.0", + "@eslint/config-helpers": "^0.4.0", + "@eslint/core": "^0.16.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.31.0", - "@eslint/plugin-kit": "^0.3.1", + "@eslint/js": "9.37.0", + "@eslint/plugin-kit": "^0.4.0", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", @@ -3062,15 +3083,16 @@ } }, "node_modules/form-data": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", - "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" }, "engines": { @@ -3224,9 +3246,9 @@ } }, "node_modules/glob/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3250,9 +3272,9 @@ } }, "node_modules/globals": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.2.0.tgz", - "integrity": "sha512-O+7l9tPdHCU320IigZZPj5zmRCFG9xHmx9cU8FqU2Rp+JN714seHV+2S9+JslCpY4gJwU2vOGox0wzgae/MCEg==", + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", + "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==", "dev": true, "license": "MIT", "engines": { @@ -3958,13 +3980,13 @@ } }, "node_modules/nock": { - "version": "14.0.5", - "resolved": "https://registry.npmjs.org/nock/-/nock-14.0.5.tgz", - "integrity": "sha512-R49fALR9caB6vxuSWUIaK2eBYeTloZQUFBZ4rHO+TbhMGQHtwnhdqKLYki+o+8qMgLvoBYWrp/2KzGPhxL4S6w==", + "version": "14.0.10", + "resolved": "https://registry.npmjs.org/nock/-/nock-14.0.10.tgz", + "integrity": "sha512-Q7HjkpyPeLa0ZVZC5qpxBt5EyLczFJ91MEewQiIi9taWuA0KB/MDJlUWtON+7dGouVdADTQsf9RA7TZk6D8VMw==", "dev": true, "license": "MIT", "dependencies": { - "@mswjs/interceptors": "^0.38.7", + "@mswjs/interceptors": "^0.39.5", "json-stringify-safe": "^5.0.1", "propagate": "^2.0.0" }, @@ -4096,16 +4118,16 @@ } }, "node_modules/open": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/open/-/open-10.1.2.tgz", - "integrity": "sha512-cxN6aIDPz6rm8hbebcP7vrQNhvRcveZoJU72Y7vskh4oIm+BZwBECnx5nTmrlres1Qapvx27Qo1Auukpf8PKXw==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", "dev": true, "license": "MIT", "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", - "is-wsl": "^3.1.0" + "wsl-utils": "^0.1.0" }, "engines": { "node": ">=18" @@ -4958,21 +4980,21 @@ } }, "node_modules/superagent": { - "version": "10.2.1", - "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.2.1.tgz", - "integrity": "sha512-O+PCv11lgTNJUzy49teNAWLjBZfc+A1enOwTpLlH6/rsvKcTwcdTT8m9azGkVqM7HBl5jpyZ7KTPhHweokBcdg==", + "version": "10.2.3", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.2.3.tgz", + "integrity": "sha512-y/hkYGeXAj7wUMjxRbB21g/l6aAEituGXM9Rwl4o20+SX3e8YOSV6BxFXl+dL3Uk0mjSL3kCbNkwURm8/gEDig==", "dev": true, "license": "MIT", "dependencies": { - "component-emitter": "^1.3.0", + "component-emitter": "^1.3.1", "cookiejar": "^2.1.4", - "debug": "^4.3.4", + "debug": "^4.3.7", "fast-safe-stringify": "^2.1.1", - "form-data": "^4.0.0", + "form-data": "^4.0.4", "formidable": "^3.5.4", "methods": "^1.1.2", "mime": "2.6.0", - "qs": "^6.11.0" + "qs": "^6.11.2" }, "engines": { "node": ">=14.18.0" @@ -4992,14 +5014,14 @@ } }, "node_modules/supertest": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.1.1.tgz", - "integrity": "sha512-aI59HBTlG9e2wTjxGJV+DygfNLgnWbGdZxiA/sgrnNNikIW8lbDvCtF6RnhZoJ82nU7qv7ZLjrvWqCEm52fAmw==", + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.1.4.tgz", + "integrity": "sha512-tjLPs7dVyqgItVFirHYqe2T+MfWc2VOBQ8QFKKbWTA3PU7liZR8zoSpAi/C1k1ilm9RsXIKYf197oap9wXGVYg==", "dev": true, "license": "MIT", "dependencies": { "methods": "^1.1.2", - "superagent": "^10.2.1" + "superagent": "^10.2.3" }, "engines": { "node": ">=14.18.0" @@ -5034,9 +5056,9 @@ } }, "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5074,14 +5096,14 @@ "license": "MIT" }, "node_modules/tinyglobby": { - "version": "0.2.14", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", - "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", "dev": true, "license": "MIT", "dependencies": { - "fdir": "^6.4.4", - "picomatch": "^4.0.2" + "fdir": "^6.5.0", + "picomatch": "^4.0.3" }, "engines": { "node": ">=12.0.0" @@ -5091,11 +5113,14 @@ } }, "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.4.6", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", - "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, "peerDependencies": { "picomatch": "^3 || ^4" }, @@ -5106,9 +5131,9 @@ } }, "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", "engines": { @@ -5270,18 +5295,18 @@ } }, "node_modules/vite": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.0.4.tgz", - "integrity": "sha512-SkaSguuS7nnmV7mfJ8l81JGBFV7Gvzp8IzgE8A8t23+AxuNX61Q5H1Tpz5efduSN7NHC8nQXD3sKQKZAu5mNEA==", + "version": "7.1.9", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.9.tgz", + "integrity": "sha512-4nVGliEpxmhCL8DslSAUdxlB6+SMrhB0a1v5ijlh1xB1nEPuy1mxaHxysVucLHuWryAxLWg6a5ei+U4TLn/rFg==", "dev": true, "license": "MIT", "dependencies": { "esbuild": "^0.25.0", - "fdir": "^6.4.6", - "picomatch": "^4.0.2", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", "postcss": "^8.5.6", - "rollup": "^4.40.0", - "tinyglobby": "^0.2.14" + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" }, "bin": { "vite": "bin/vite.js" @@ -5368,11 +5393,14 @@ } }, "node_modules/vite/node_modules/fdir": { - "version": "6.4.6", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", - "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, "peerDependencies": { "picomatch": "^3 || ^4" }, @@ -5383,9 +5411,9 @@ } }, "node_modules/vite/node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", "engines": { @@ -5629,6 +5657,22 @@ "dev": true, "license": "ISC" }, + "node_modules/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index 71b4955..d6990d5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mcp-hub", - "version": "4.2.1", + "version": "5.0.2", "description": "A manager server for MCP servers that handles process management and tool routing", "author": "Ravitemer", "license": "MIT", @@ -42,21 +42,21 @@ "prepublishOnly": "npm run build" }, "devDependencies": { - "@eslint/js": "^9.31.0", - "@modelcontextprotocol/sdk": "^1.15.1", + "@eslint/js": "^9.37.0", + "@modelcontextprotocol/sdk": "^1.20.0", "@vitest/coverage-v8": "^3.2.4", "chokidar": "^4.0.3", - "esbuild": "^0.25.3", - "eslint": "^9.31.0", + "esbuild": "^0.25.10", + "eslint": "^9.37.0", "express": "^4.21.2", "fast-deep-equal": "^3.1.3", - "globals": "^16.2.0", + "globals": "^16.4.0", "mock-fs": "^5.5.0", - "nock": "^14.0.5", + "nock": "^14.0.10", "nodemon": "^3.1.10", - "open": "^10.1.2", + "open": "^10.2.0", "reconnecting-eventsource": "^1.6.4", - "supertest": "^7.1.1", + "supertest": "^7.1.4", "uuid": "^11.1.0", "vitest": "^3.2.4", "yargs": "^17.7.2" diff --git a/src/mcp/server.js b/src/mcp/server.js index cf5d638..c2cefa3 100644 --- a/src/mcp/server.js +++ b/src/mcp/server.js @@ -30,6 +30,8 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; +import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { randomUUID } from "crypto"; import { ListToolsRequestSchema, CallToolRequestSchema, @@ -514,6 +516,7 @@ export class MCPServerEndpoint { /** * Handle MCP messages (POST /messages) + * Legacy SSE transport endpoint */ async handleMCPMessage(req, res) { const sessionId = req.query.sessionId; @@ -542,6 +545,97 @@ export class MCPServerEndpoint { } } + /** + * Handle Streamable HTTP transport requests (new MCP protocol) + * Supports both POST and GET requests on a single endpoint + */ + async handleStreamableHTTP(req, res) { + try { + // Check if this is for an existing session + const sessionId = req.headers['mcp-session-id']; + + if (sessionId) { + // Reuse existing transport for this session + const clientInfo = this.clients.get(sessionId); + if (clientInfo) { + await clientInfo.transport.handleRequest(req, res, req.body); + return; + } + // Session not found - will create new one below + logger.debug(`Session ${sessionId} not found, creating new session`); + } + + // Create new transport and server for new session + const transport = new StreamableHTTPServerTransport({ + // Generate cryptographically secure session IDs + sessionIdGenerator: () => randomUUID(), + + // DNS rebinding protection - disabled by default for local development + // Enable in production with appropriate allowedOrigins/allowedHosts configuration + enableDnsRebindingProtection: false, + }); + + // Create a new server instance for this session + const server = this.createServer(); + + let clientInfo; + + // Setup cleanup for when transport closes + const cleanup = async () => { + if (transport.sessionId) { + this.clients.delete(transport.sessionId); + } + try { + await server.close(); + } catch (error) { + logger.warn(`Error closing server: ${error.message}`); + } finally { + logger.info(`'${clientInfo?.name ?? "Unknown"}' client disconnected from MCP HUB (Streamable HTTP)`); + } + }; + + transport.onclose = cleanup; + + // Connect MCP server to transport BEFORE handling the request + await server.connect(transport); + + server.oninitialized = () => { + clientInfo = server.getClientVersion(); + if (clientInfo) { + logger.info(`'${clientInfo.name}' client connected to MCP HUB (Streamable HTTP)`); + } + }; + + // Store transport and server together using the transport's session ID + // Note: sessionId will be set by the transport during handleRequest if it's an initialize request + const originalHandleRequest = transport.handleRequest.bind(transport); + transport.handleRequest = async (req, res, parsedBody) => { + await originalHandleRequest(req, res, parsedBody); + // After handling, if a session was created, store it + if (transport.sessionId && !this.clients.has(transport.sessionId)) { + logger.debug(`Streamable HTTP session created: ${transport.sessionId}`); + this.clients.set(transport.sessionId, { transport, server }); + } + }; + + // Handle the HTTP request + await transport.handleRequest(req, res, req.body); + + } catch (error) { + logger.warn(`Error handling Streamable HTTP request: ${error.message}`); + if (!res.headersSent) { + res.status(500).json({ + jsonrpc: "2.0", + error: { + code: -32603, + message: "Internal error", + }, + id: null, + }); + } + } + } + /** * Get statistics about the MCP endpoint */ diff --git a/src/server.js b/src/server.js index 64265a4..122f8f9 100644 --- a/src/server.js +++ b/src/server.js @@ -342,22 +342,56 @@ registerRoute("GET", "/events", "Subscribe to server events", async (req, res) = } }); -// Register MCP SSE endpoint +// Register unified MCP endpoint for both Streamable HTTP (new) and SSE (legacy) +app.post("/mcp", async (req, res) => { + try { + if (!mcpServerEndpoint) { + throw new ServerError("MCP server endpoint not initialized"); + } + // POST requests use Streamable HTTP transport (new protocol) + await mcpServerEndpoint.handleStreamableHTTP(req, res); + } catch (error) { + logger.warn(`Failed to handle MCP POST request: ${error.message}`); + if (!res.headersSent) { + res.status(500).json({ + jsonrpc: "2.0", + error: { + code: -32603, + message: "Internal error", + }, + id: null, + }); + } + } +}); + app.get("/mcp", async (req, res) => { try { if (!mcpServerEndpoint) { throw new ServerError("MCP server endpoint not initialized"); } - await mcpServerEndpoint.handleSSEConnection(req, res); + + // Check if this is a Streamable HTTP GET (has Mcp-Session-Id header) + // or legacy SSE (Accept: text/event-stream header) + const sessionId = req.headers['mcp-session-id']; + const acceptsSSE = req.headers.accept?.includes('text/event-stream'); + + if (sessionId || !acceptsSSE) { + // Streamable HTTP GET request (for server-to-client messages in active session) + await mcpServerEndpoint.handleStreamableHTTP(req, res); + } else { + // Legacy SSE transport (backward compatibility) + await mcpServerEndpoint.handleSSEConnection(req, res); + } } catch (error) { - logger.warn(`Failed to setup MCP SSE connection: ${error.message}`) + logger.warn(`Failed to handle MCP GET request: ${error.message}`); if (!res.headersSent) { res.status(500).send('Error establishing MCP connection'); } } }); -// Register MCP messages endpoint +// Register legacy MCP messages endpoint (for SSE transport backward compatibility) app.post("/messages", async (req, res) => { try { if (!mcpServerEndpoint) { From c38fdd2788314147596f955d2bdc4684efd783a5 Mon Sep 17 00:00:00 2001 From: Bernardo Donadio Date: Wed, 19 Nov 2025 12:48:28 -0300 Subject: [PATCH 2/2] fix: resolve SSE connection issues and prevent stack overflow on cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit addresses three critical issues and adds comprehensive test coverage: ## Fixes 1. **SSE Connection Routing** - Fixed 406 errors for SSE clients with non-standard Accept headers - Changed GET /mcp routing logic to default to SSE transport for any sessionless request - Previously rejected clients sending `Accept: */*` instead of `Accept: text/event-stream` - Now properly supports legacy clients like Kilo Code that don't send proper Accept headers - Resolves "SSE error: Non-200 status code (406)" connection failures 2. **Transport Cleanup Stack Overflow** - Fixed infinite recursion in cleanup handlers - Added cleanup guard flags to prevent circular cleanup calls - Issue occurred when `cleanup()` → `server.close()` → `transport.close()` → `onclose` → `cleanup()` created infinite loop - Affected both SSE and Streamable HTTP transports during graceful shutdown - Resolves "RangeError: Maximum call stack size exceeded" errors on client disconnect 3. **MCP Server Logging** - Improved stderr logging level for MCP server output - Changed MCP server stderr output from `warn` to `debug` level - Prevents normal informational messages from appearing as warnings - Improves log clarity by distinguishing between actual warnings and routine server output ## Tests Added comprehensive test coverage (20 passing tests): - **tests/server-routing.test.js** (13 tests) - Tests GET /mcp endpoint routing logic - Verifies SSE routing for sessionless requests regardless of Accept header - Tests backward compatibility with clients sending `Accept: */*` - Validates 406 error prevention for legacy clients - **tests/mcp-server.test.js** (7 tests) - Tests cleanup guard flags in both SSE and Streamable HTTP transports - Verifies prevention of infinite recursion during cleanup - Tests session management and cleanup behavior - **tests/MCPConnection.test.js** (updated) - Enhanced logger mock with debug method - Updated stderr test to verify debug logging instead of warn ## Changes - src/server.js: Updated GET /mcp routing to default to SSE for sessionless requests - src/mcp/server.js: Added cleanup guard flags to prevent recursion - src/MCPConnection.js: Changed stderr logging from warn to debug level - CHANGELOG.md: Added release notes for v5.0.3 - package.json: Bumped version to 5.0.3 - README.md: Updated transport documentation - tests/: Added comprehensive test coverage 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- CHANGELOG.md | 21 +++ README.md | 32 +++-- package.json | 2 +- src/MCPConnection.js | 2 +- src/mcp/server.js | 92 +++++++++---- src/server.js | 74 ++++++++++- tests/MCPConnection.test.js | 23 +++- tests/mcp-server.test.js | 248 +++++++++++++++++++++++++++++++++++ tests/server-routing.test.js | 228 ++++++++++++++++++++++++++++++++ 9 files changed, 679 insertions(+), 43 deletions(-) create mode 100644 tests/mcp-server.test.js create mode 100644 tests/server-routing.test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 06afea7..99e9e14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,27 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [5.0.3] - 2025-11-19 + +### Fixed + +- **SSE Connection Routing**: Fixed 406 errors for SSE clients with non-standard Accept headers + - Changed GET /mcp routing logic to default to SSE transport for any sessionless request + - Previously rejected clients sending `Accept: */*` instead of `Accept: text/event-stream` + - Now properly supports legacy clients like Kilo Code that don't send proper Accept headers + - Resolves "SSE error: Non-200 status code (406)" connection failures + +- **Transport Cleanup Stack Overflow**: Fixed infinite recursion in cleanup handlers + - Added cleanup guard flags to prevent circular cleanup calls + - Issue occurred when `cleanup()` → `server.close()` → `transport.close()` → `onclose` → `cleanup()` created infinite loop + - Affected both SSE and Streamable HTTP transports during graceful shutdown + - Resolves "RangeError: Maximum call stack size exceeded" errors on client disconnect + +- **MCP Server Logging**: Improved stderr logging level for MCP server output + - Changed MCP server stderr output from `warn` to `debug` level + - Prevents normal informational messages from appearing as warnings + - Improves log clarity by distinguishing between actual warnings and routine server output + ## [5.0.2] - 2025-10-10 ### Fixed diff --git a/README.md b/README.md index 6d00add..73bc8b7 100644 --- a/README.md +++ b/README.md @@ -16,8 +16,8 @@ This dual-interface approach means you can manage servers through the Hub's UI w | Category | Feature | Support | Notes | |----------|---------|---------|-------| | **Transport** |||| -| | streamable-http | ✅ | Primary transport protocol for remote servers | -| | SSE | ✅ | Fallback transport for remote servers | +| | streamable-http | ✅ | Primary transport protocol (client ↔ hub, hub ↔ servers) | +| | SSE | ✅ | Legacy transport for backward compatibility | | | STDIO | ✅ | For running local servers | | **Authentication** |||| | | OAuth 2.0 | ✅ | With PKCE flow | @@ -55,12 +55,16 @@ Configure all MCP clients with just one endpoint: { "mcpServers" : { "Hub": { - "url" : "http://localhost:37373/mcp" + "url" : "http://localhost:37373/mcp" } } } ``` +**Transport Auto-Detection**: The Hub automatically detects which transport protocol your client uses: +- **Modern clients**: Streamable HTTP transport (MCP 2025-03-26 spec) +- **Legacy clients**: SSE transport (automatic fallback for backward compatibility) + The Hub automatically: - Namespaces capabilities to prevent conflicts (e.g., `filesystem__search` vs `database__search`) - Routes requests to the appropriate server @@ -76,10 +80,20 @@ The Hub automatically: - Real-time capability updates when servers change - Simplified client configuration - just one endpoint instead of many +- **Modern Transport Layer**: + - **Streamable HTTP**: Primary transport using MCP 2025-03-26 specification + - Single unified endpoint for all client requests + - Cryptographically secure session management + - Optional DNS rebinding protection (configurable) + - Efficient bidirectional communication + - **SSE Fallback**: Automatic backward compatibility for legacy clients + - Seamless detection and fallback + - No configuration required + - **Dynamic Server Management**: - Start, stop, enable/disable servers on demand - Real-time configuration updates with automatic server reconnection - - Support for local (STDIO) and remote (streamable-http/SSE) MCP servers + - Support for local (STDIO) and remote (streamable-http/SSE) MCP servers - Health monitoring and automatic recovery - OAuth authentication with PKCE flow - Header-based token authentication @@ -126,12 +140,14 @@ The main management server that: #### MCP Servers Connected services that: - Provide tools, resources, templates, and prompts -- Support two connectivity modes: - - Script-based STDIO servers for local operations - - Remote servers (streamable-http/SSE) with OAuth support +- Support three connectivity modes: + - **STDIO**: Script-based servers for local operations + - **Streamable HTTP**: Modern remote servers (MCP 2025-03-26) + - **SSE**: Legacy remote servers (backward compatibility) - Implement real-time capability updates - Support automatic status recovery -- Maintain consistent interface across transport types +- OAuth authentication support for remote servers +- Maintain consistent interface across all transport types ## Installation diff --git a/package.json b/package.json index d6990d5..5b52518 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mcp-hub", - "version": "5.0.2", + "version": "5.0.3", "description": "A manager server for MCP servers that handles process management and tool routing", "author": "Ravitemer", "license": "MIT", diff --git a/src/MCPConnection.js b/src/MCPConnection.js index 4ec92f1..04b6581 100644 --- a/src/MCPConnection.js +++ b/src/MCPConnection.js @@ -682,7 +682,7 @@ export class MCPConnection extends EventEmitter { if (stderrStream) { stderrStream.on("data", (data) => { const errorOutput = data.toString().trim(); - logger.warn(`${this.name} stderr: ${errorOutput}`) + logger.debug(`${this.name} stderr: ${errorOutput}`) }); } return transport diff --git a/src/mcp/server.js b/src/mcp/server.js index c2cefa3..153c81e 100644 --- a/src/mcp/server.js +++ b/src/mcp/server.js @@ -475,42 +475,65 @@ export class MCPServerEndpoint { * Handle SSE transport creation (GET /mcp) */ async handleSSEConnection(req, res) { + logger.debug('handleSSEConnection called', { + url: req.url, + headers: req.headers, + method: req.method + }); - // Create SSE transport - const transport = new SSEServerTransport('/messages', res); - const sessionId = transport.sessionId; + try { + // Create SSE transport + logger.debug('Creating SSE transport'); + const transport = new SSEServerTransport('/messages', res); + const sessionId = transport.sessionId; + logger.debug('SSE transport created', { sessionId }); - // Create a new server instance for this connection - const server = this.createServer(); + // Create a new server instance for this connection + const server = this.createServer(); + logger.debug('Server instance created for SSE connection'); - // Store transport and server together - this.clients.set(sessionId, { transport, server }); + // Store transport and server together + this.clients.set(sessionId, { transport, server }); - let clientInfo + let clientInfo + let cleanupCalled = false; - // Setup cleanup on close - const cleanup = async () => { - this.clients.delete(sessionId); - try { - await server.close(); - } catch (error) { - logger.warn(`Error closing server connected to ${clientInfo?.name ?? "Unknown"}: ${error.message}`); - } finally { - logger.info(`'${clientInfo?.name ?? "Unknown"}' client disconnected from MCP HUB`); - } - }; + // Setup cleanup on close + const cleanup = async () => { + if (cleanupCalled) return; + cleanupCalled = true; - res.on("close", cleanup); - transport.onclose = cleanup; + this.clients.delete(sessionId); + try { + await server.close(); + } catch (error) { + logger.warn(`Error closing server connected to ${clientInfo?.name ?? "Unknown"}: ${error.message}`); + } finally { + logger.info(`'${clientInfo?.name ?? "Unknown"}' client disconnected from MCP HUB`); + } + }; + + res.on("close", cleanup); + transport.onclose = cleanup; - // Connect MCP server to transport - await server.connect(transport); - server.oninitialized = () => { - clientInfo = server.getClientVersion() - if (clientInfo) { - logger.info(`'${clientInfo.name}' client connected to MCP HUB`) + // Connect MCP server to transport + logger.debug('Connecting server to SSE transport'); + await server.connect(transport); + logger.debug('Server connected to SSE transport successfully'); + + server.oninitialized = () => { + clientInfo = server.getClientVersion() + if (clientInfo) { + logger.info(`'${clientInfo.name}' client connected to MCP HUB`) + } } + } catch (error) { + logger.error('SSE_CONNECTION_ERROR', 'Error in handleSSEConnection', { + error: error.message, + stack: error.stack + }); + throw error; } } @@ -550,19 +573,30 @@ export class MCPServerEndpoint { * Supports both POST and GET requests on a single endpoint */ async handleStreamableHTTP(req, res) { + logger.debug('handleStreamableHTTP called', { + method: req.method, + url: req.url, + headers: req.headers, + sessionId: req.headers['mcp-session-id'] || 'none' + }); + try { // Check if this is for an existing session const sessionId = req.headers['mcp-session-id']; if (sessionId) { + logger.debug('Session ID found in request', { sessionId }); // Reuse existing transport for this session const clientInfo = this.clients.get(sessionId); if (clientInfo) { + logger.debug('Reusing existing session', { sessionId }); await clientInfo.transport.handleRequest(req, res, req.body); return; } // Session not found - will create new one below logger.debug(`Session ${sessionId} not found, creating new session`); + } else { + logger.debug('No session ID in request, will create new session'); } // Create new transport and server for new session @@ -579,9 +613,13 @@ export class MCPServerEndpoint { const server = this.createServer(); let clientInfo; + let cleanupCalled = false; // Setup cleanup for when transport closes const cleanup = async () => { + if (cleanupCalled) return; + cleanupCalled = true; + if (transport.sessionId) { this.clients.delete(transport.sessionId); } diff --git a/src/server.js b/src/server.js index 122f8f9..32cf9d4 100644 --- a/src/server.js +++ b/src/server.js @@ -22,6 +22,18 @@ const SERVER_ID = "mcp-hub"; // Create Express app const app = express(); app.use(express.json()); + +// Log all incoming requests to /api routes +router.use((req, res, next) => { + logger.debug('API request received', { + method: req.method, + path: req.path, + url: req.url, + headers: req.headers + }); + next(); +}); + app.use("/api", router); // Helper to determine HTTP status code from error type @@ -321,12 +333,21 @@ class ServiceManager { // Register SSE endpoint registerRoute("GET", "/events", "Subscribe to server events", async (req, res) => { + logger.debug('SSE connection attempt', { + url: req.url, + path: req.path, + headers: req.headers, + method: req.method + }); + try { if (!serviceManager?.sseManager) { + logger.error('SSE_MANAGER_NOT_INITIALIZED', 'SSE manager not initialized when client attempted to connect'); throw new ServerError("SSE manager not initialized"); } // Add client connection const connection = await serviceManager.sseManager.addConnection(req, res); + logger.debug('SSE connection established successfully'); // Send initial state connection.send(EventTypes.HUB_STATE, serviceManager.getState()); } catch (error) { @@ -344,14 +365,26 @@ registerRoute("GET", "/events", "Subscribe to server events", async (req, res) = // Register unified MCP endpoint for both Streamable HTTP (new) and SSE (legacy) app.post("/mcp", async (req, res) => { + logger.debug('POST /mcp request received', { + url: req.url, + headers: req.headers, + contentType: req.headers['content-type'], + body: req.body + }); + try { if (!mcpServerEndpoint) { + logger.error('MCP_ENDPOINT_NOT_INITIALIZED', 'MCP server endpoint not initialized'); throw new ServerError("MCP server endpoint not initialized"); } // POST requests use Streamable HTTP transport (new protocol) + logger.debug('Routing POST to Streamable HTTP handler'); await mcpServerEndpoint.handleStreamableHTTP(req, res); } catch (error) { - logger.warn(`Failed to handle MCP POST request: ${error.message}`); + logger.warn(`Failed to handle MCP POST request: ${error.message}`, { + error: error.message, + stack: error.stack + }); if (!res.headersSent) { res.status(500).json({ jsonrpc: "2.0", @@ -366,25 +399,47 @@ app.post("/mcp", async (req, res) => { }); app.get("/mcp", async (req, res) => { + logger.debug('GET /mcp request received', { + url: req.url, + headers: req.headers, + method: req.method, + query: req.query + }); + try { if (!mcpServerEndpoint) { + logger.error('MCP_ENDPOINT_NOT_INITIALIZED', 'MCP server endpoint not initialized'); throw new ServerError("MCP server endpoint not initialized"); } // Check if this is a Streamable HTTP GET (has Mcp-Session-Id header) - // or legacy SSE (Accept: text/event-stream header) + // or legacy SSE (no session ID means initial connection attempt) const sessionId = req.headers['mcp-session-id']; const acceptsSSE = req.headers.accept?.includes('text/event-stream'); - if (sessionId || !acceptsSSE) { + logger.debug('MCP GET request type detection', { + sessionId: sessionId || 'none', + accept: req.headers.accept || 'none', + acceptsSSE, + willUseStreamableHTTP: !!sessionId, + willUseSSE: !sessionId + }); + + if (sessionId) { // Streamable HTTP GET request (for server-to-client messages in active session) + logger.debug('Routing to Streamable HTTP handler (has session ID)'); await mcpServerEndpoint.handleStreamableHTTP(req, res); } else { // Legacy SSE transport (backward compatibility) + // Any GET request without a session ID is assumed to be an SSE connection attempt + logger.debug('Routing to SSE handler (no session ID)'); await mcpServerEndpoint.handleSSEConnection(req, res); } } catch (error) { - logger.warn(`Failed to handle MCP GET request: ${error.message}`); + logger.warn(`Failed to handle MCP GET request: ${error.message}`, { + error: error.message, + stack: error.stack + }); if (!res.headersSent) { res.status(500).send('Error establishing MCP connection'); } @@ -951,9 +1006,18 @@ router.use((err, req, res, next) => { method: req.method, }); + const statusCode = getStatusCode(error); + logger.warn('Router error handler triggered', { + path: req.path, + method: req.method, + statusCode, + error: error.message, + code: error.code + }); + // Only send error response if headers haven't been sent if (!res.headersSent) { - res.status(getStatusCode(error)).json({ + res.status(statusCode).json({ error: error.message, code: error.code, data: error.data, diff --git a/tests/MCPConnection.test.js b/tests/MCPConnection.test.js index 59394e7..0a73163 100644 --- a/tests/MCPConnection.test.js +++ b/tests/MCPConnection.test.js @@ -35,6 +35,7 @@ vi.mock("../src/utils/logger.js", () => ({ info: vi.fn(), error: vi.fn(), warn: vi.fn(), + debug: vi.fn(), }, })); @@ -129,16 +130,36 @@ describe("MCPConnection", () => { expect(connection.startTime).toBeNull(); }); - it("should handle stderr output", async () => { + it("should handle stderr output and log at debug level", async () => { + const logger = (await import("../src/utils/logger.js")).default; let stderrCallback; transport.stderr.on.mockImplementation((event, cb) => { if (event === "data") stderrCallback = cb; }); + // Mock successful connection to avoid the connection error + client.connect.mockResolvedValueOnce(undefined); + client.request.mockResolvedValueOnce({ + capabilities: {}, + protocolVersion: "1.0", + serverInfo: { name: "test-server", version: "1.0" }, + }); + await connection.connect(); + // Trigger stderr output stderrCallback(Buffer.from("Error output")); + + // Verify error was stored expect(connection.error).toBe("Error output"); + + // Verify logger.debug was called (not logger.warn) + expect(logger.debug).toHaveBeenCalledWith( + expect.stringContaining("stderr: Error output") + ); + expect(logger.warn).not.toHaveBeenCalledWith( + expect.stringContaining("stderr:") + ); }); it("should disconnect cleanly", async () => { diff --git a/tests/mcp-server.test.js b/tests/mcp-server.test.js new file mode 100644 index 0000000..8cc7411 --- /dev/null +++ b/tests/mcp-server.test.js @@ -0,0 +1,248 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { MCPServerEndpoint } from "../src/mcp/server.js"; +import { Server } from "@modelcontextprotocol/sdk/server/index.js"; +import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; +import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; + +// Mock MCP SDK +vi.mock("@modelcontextprotocol/sdk/server/index.js"); +vi.mock("@modelcontextprotocol/sdk/server/sse.js"); +vi.mock("@modelcontextprotocol/sdk/server/streamableHttp.js"); + +// Mock logger +vi.mock("../src/utils/logger.js", () => ({ + default: { + info: vi.fn(), + error: vi.fn(), + warn: vi.fn(), + debug: vi.fn(), + }, +})); + +// Mock MCPHub +vi.mock("../src/MCPHub.js", () => ({ + MCPHub: vi.fn(() => ({ + on: vi.fn(), + rawRequest: vi.fn(), + })), +})); + +describe("MCPServerEndpoint", () => { + let endpoint; + let mockMCPHub; + let mockServer; + let mockTransport; + let mockReq; + let mockRes; + + beforeEach(async () => { + vi.clearAllMocks(); + + // Mock MCP Hub + const { MCPHub } = await import("../src/MCPHub.js"); + mockMCPHub = { + on: vi.fn(), + rawRequest: vi.fn(), + getAllServerStatuses: vi.fn().mockReturnValue({}), + connections: new Map(), // Add connections map for syncCapabilities + }; + MCPHub.mockReturnValue(mockMCPHub); + + // Mock Server + mockServer = { + connect: vi.fn().mockResolvedValue(undefined), + close: vi.fn().mockResolvedValue(undefined), + setRequestHandler: vi.fn(), + getClientVersion: vi.fn().mockReturnValue({ name: "test-client" }), + oninitialized: null, + onerror: null, + }; + Server.mockImplementation(() => mockServer); + + // Mock SSE Transport + mockTransport = { + sessionId: "test-session-id", + onclose: null, + close: vi.fn().mockResolvedValue(undefined), + }; + SSEServerTransport.mockImplementation(() => mockTransport); + + // Mock Streamable HTTP Transport + StreamableHTTPServerTransport.mockImplementation(() => ({ + sessionId: "streamable-session-id", + onclose: null, + close: vi.fn().mockResolvedValue(undefined), + handleRequest: vi.fn().mockResolvedValue(undefined), + })); + + // Create endpoint instance + endpoint = new MCPServerEndpoint(mockMCPHub); + + // Mock request and response + mockReq = { + url: "/mcp", + headers: {}, + on: vi.fn(), + }; + + mockRes = { + on: vi.fn(), + setHeader: vi.fn(), + write: vi.fn(), + end: vi.fn(), + writableEnded: false, + }; + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe("SSE Connection Cleanup", () => { + it("should prevent infinite recursion with cleanup guard", async () => { + // Simulate SSE connection + await endpoint.handleSSEConnection(mockReq, mockRes); + + // Get the cleanup function by triggering the onclose handler + const cleanupFn = mockTransport.onclose; + expect(cleanupFn).toBeDefined(); + + // Call cleanup multiple times - should only execute once due to guard + await cleanupFn(); + await cleanupFn(); + await cleanupFn(); + + // Server.close should only be called once, not three times + expect(mockServer.close).toHaveBeenCalledTimes(1); + }); + + it("should handle cleanup when response closes", async () => { + await endpoint.handleSSEConnection(mockReq, mockRes); + + // Get the response close handler + const resCloseHandler = mockRes.on.mock.calls.find( + (call) => call[0] === "close" + )?.[1]; + expect(resCloseHandler).toBeDefined(); + + // Trigger response close + await resCloseHandler(); + + // Should call server.close + expect(mockServer.close).toHaveBeenCalled(); + }); + + it("should not throw when cleanup is called during server.close", async () => { + // Mock server.close to trigger transport.close which triggers cleanup + mockServer.close.mockImplementation(async () => { + if (mockTransport.onclose) { + await mockTransport.onclose(); + } + }); + + await endpoint.handleSSEConnection(mockReq, mockRes); + + // This should not cause infinite recursion or throw + const cleanupFn = mockTransport.onclose; + await expect(cleanupFn()).resolves.not.toThrow(); + + // Should still only call server.close once + expect(mockServer.close).toHaveBeenCalledTimes(1); + }); + }); + + describe("Streamable HTTP Cleanup", () => { + let streamableTransport; + + beforeEach(() => { + streamableTransport = { + sessionId: null, + onclose: null, + close: vi.fn().mockResolvedValue(undefined), + handleRequest: vi.fn().mockImplementation(async (req, res, body) => { + // Simulate session ID being set during handleRequest + if (!streamableTransport.sessionId) { + streamableTransport.sessionId = "streamable-session-id"; + } + }), + }; + StreamableHTTPServerTransport.mockImplementation( + () => streamableTransport + ); + }); + + it("should prevent infinite recursion in streamable HTTP cleanup", async () => { + await endpoint.handleStreamableHTTP(mockReq, mockRes); + + // Get the cleanup function + const cleanupFn = streamableTransport.onclose; + expect(cleanupFn).toBeDefined(); + + // Call cleanup multiple times + await cleanupFn(); + await cleanupFn(); + await cleanupFn(); + + // Server.close should only be called once + expect(mockServer.close).toHaveBeenCalledTimes(1); + }); + + it("should handle cleanup when transport closes during server.close", async () => { + // Mock server.close to trigger transport.close + mockServer.close.mockImplementation(async () => { + if (streamableTransport.onclose) { + await streamableTransport.onclose(); + } + }); + + await endpoint.handleStreamableHTTP(mockReq, mockRes); + + const cleanupFn = streamableTransport.onclose; + await expect(cleanupFn()).resolves.not.toThrow(); + expect(mockServer.close).toHaveBeenCalledTimes(1); + }); + }); + + describe("Session Management", () => { + it("should clean up session from clients map on SSE disconnect", async () => { + await endpoint.handleSSEConnection(mockReq, mockRes); + + // Verify session was added + expect(endpoint.clients.has("test-session-id")).toBe(true); + + // Trigger cleanup + const cleanupFn = mockTransport.onclose; + await cleanupFn(); + + // Verify session was removed + expect(endpoint.clients.has("test-session-id")).toBe(false); + }); + + it("should clean up session from clients map on Streamable HTTP disconnect", async () => { + const streamableTransport = { + sessionId: "streamable-session-id", + onclose: null, + close: vi.fn().mockResolvedValue(undefined), + handleRequest: vi.fn().mockResolvedValue(undefined), + }; + StreamableHTTPServerTransport.mockImplementation( + () => streamableTransport + ); + + await endpoint.handleStreamableHTTP(mockReq, mockRes); + + // Manually add to clients map (normally done by handleRequest wrapper) + endpoint.clients.set("streamable-session-id", { + transport: streamableTransport, + server: mockServer, + }); + + // Trigger cleanup + const cleanupFn = streamableTransport.onclose; + await cleanupFn(); + + // Verify session was removed + expect(endpoint.clients.has("streamable-session-id")).toBe(false); + }); + }); +}); diff --git a/tests/server-routing.test.js b/tests/server-routing.test.js new file mode 100644 index 0000000..ffb3b94 --- /dev/null +++ b/tests/server-routing.test.js @@ -0,0 +1,228 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import request from "supertest"; +import express from "express"; + +// Mock dependencies +vi.mock("../src/utils/logger.js", () => ({ + default: { + info: vi.fn(), + error: vi.fn(), + warn: vi.fn(), + debug: vi.fn(), + setSseManager: vi.fn(), + }, +})); + +vi.mock("../src/MCPHub.js", () => ({ + MCPHub: vi.fn(() => ({ + on: vi.fn(), + connect: vi.fn(), + disconnect: vi.fn(), + getAllServerStatuses: vi.fn().mockReturnValue({}), + })), +})); + +vi.mock("../src/mcp/server.js", () => ({ + MCPServerEndpoint: vi.fn(), +})); + +vi.mock("../src/marketplace.js", () => ({ + getMarketplace: vi.fn(() => ({ + initialize: vi.fn(), + })), +})); + +vi.mock("../src/utils/workspace-cache.js", () => ({ + WorkspaceCacheManager: vi.fn(() => ({ + register: vi.fn(), + updateActiveConnections: vi.fn(), + })), +})); + +describe("Server Routing - GET /mcp", () => { + let app; + let mockMCPServerEndpoint; + + beforeEach(() => { + vi.clearAllMocks(); + + // Create express app + app = express(); + app.use(express.json()); + + // Mock MCP server endpoint + mockMCPServerEndpoint = { + handleSSEConnection: vi.fn().mockImplementation(async (req, res) => { + res.setHeader("Content-Type", "text/event-stream"); + res.write("data: SSE connection established\n\n"); + res.end(); + }), + handleStreamableHTTP: vi.fn().mockImplementation(async (req, res) => { + res.status(200).json({ status: "Streamable HTTP" }); + }), + }; + + // Setup GET /mcp route with the routing logic from server.js + app.get("/mcp", async (req, res) => { + try { + const sessionId = req.headers["mcp-session-id"]; + const acceptsSSE = req.headers.accept?.includes("text/event-stream"); + + if (sessionId) { + // Streamable HTTP GET request (for server-to-client messages in active session) + await mockMCPServerEndpoint.handleStreamableHTTP(req, res); + } else { + // Legacy SSE transport (backward compatibility) + // Any GET request without a session ID is assumed to be an SSE connection attempt + await mockMCPServerEndpoint.handleSSEConnection(req, res); + } + } catch (error) { + if (!res.headersSent) { + res.status(500).send("Error establishing MCP connection"); + } + } + }); + }); + + describe("SSE Routing", () => { + it("should route to SSE handler when Accept header is text/event-stream", async () => { + const response = await request(app) + .get("/mcp") + .set("Accept", "text/event-stream") + .expect(200); + + expect(mockMCPServerEndpoint.handleSSEConnection).toHaveBeenCalled(); + expect(mockMCPServerEndpoint.handleStreamableHTTP).not.toHaveBeenCalled(); + expect(response.headers["content-type"]).toContain("text/event-stream"); + }); + + it("should route to SSE handler when Accept header is */* (wildcard)", async () => { + await request(app).get("/mcp").set("Accept", "*/*").expect(200); + + expect(mockMCPServerEndpoint.handleSSEConnection).toHaveBeenCalled(); + expect(mockMCPServerEndpoint.handleStreamableHTTP).not.toHaveBeenCalled(); + }); + + it("should route to SSE handler when no Accept header is provided", async () => { + await request(app).get("/mcp").expect(200); + + expect(mockMCPServerEndpoint.handleSSEConnection).toHaveBeenCalled(); + expect(mockMCPServerEndpoint.handleStreamableHTTP).not.toHaveBeenCalled(); + }); + + it("should route to SSE handler for non-standard Accept headers", async () => { + await request(app) + .get("/mcp") + .set("Accept", "application/json") + .expect(200); + + expect(mockMCPServerEndpoint.handleSSEConnection).toHaveBeenCalled(); + expect(mockMCPServerEndpoint.handleStreamableHTTP).not.toHaveBeenCalled(); + }); + + it("should route to SSE handler when no session ID is present", async () => { + await request(app) + .get("/mcp") + .set("Accept", "text/html") + .expect(200); + + expect(mockMCPServerEndpoint.handleSSEConnection).toHaveBeenCalled(); + expect(mockMCPServerEndpoint.handleStreamableHTTP).not.toHaveBeenCalled(); + }); + }); + + describe("Streamable HTTP Routing", () => { + it("should route to Streamable HTTP handler when session ID is present", async () => { + const response = await request(app) + .get("/mcp") + .set("mcp-session-id", "test-session-123") + .set("Accept", "*/*") + .expect(200); + + expect(mockMCPServerEndpoint.handleStreamableHTTP).toHaveBeenCalled(); + expect(mockMCPServerEndpoint.handleSSEConnection).not.toHaveBeenCalled(); + expect(response.body).toEqual({ status: "Streamable HTTP" }); + }); + + it("should route to Streamable HTTP even with text/event-stream if session ID present", async () => { + await request(app) + .get("/mcp") + .set("mcp-session-id", "test-session-456") + .set("Accept", "text/event-stream") + .expect(200); + + expect(mockMCPServerEndpoint.handleStreamableHTTP).toHaveBeenCalled(); + expect(mockMCPServerEndpoint.handleSSEConnection).not.toHaveBeenCalled(); + }); + }); + + describe("Backward Compatibility", () => { + it("should support legacy clients like Kilo Code with Accept: */*", async () => { + // Kilo Code sends Accept: */* without session ID + const response = await request(app) + .get("/mcp") + .set("Accept", "*/*") + .set("User-Agent", "node") + .expect(200); + + // Should route to SSE, not return 406 + expect(mockMCPServerEndpoint.handleSSEConnection).toHaveBeenCalled(); + expect(response.status).not.toBe(406); + }); + + it("should not return 406 for clients without proper Accept headers", async () => { + const response = await request(app) + .get("/mcp") + .set("User-Agent", "test-client") + .expect(200); + + expect(response.status).not.toBe(406); + expect(mockMCPServerEndpoint.handleSSEConnection).toHaveBeenCalled(); + }); + }); + + describe("Request Validation", () => { + it("should handle requests with multiple Accept values", async () => { + await request(app) + .get("/mcp") + .set("Accept", "application/json, text/event-stream, */*") + .expect(200); + + // Should still route to SSE since no session ID + expect(mockMCPServerEndpoint.handleSSEConnection).toHaveBeenCalled(); + }); + + it("should prioritize session ID over Accept header", async () => { + await request(app) + .get("/mcp") + .set("mcp-session-id", "priority-test") + .set("Accept", "*/*") + .expect(200); + + // Should route to Streamable HTTP because session ID is present + expect(mockMCPServerEndpoint.handleStreamableHTTP).toHaveBeenCalled(); + expect(mockMCPServerEndpoint.handleSSEConnection).not.toHaveBeenCalled(); + }); + }); + + describe("Error Handling", () => { + it("should handle SSE connection errors gracefully", async () => { + mockMCPServerEndpoint.handleSSEConnection.mockRejectedValueOnce( + new Error("SSE connection failed") + ); + + await request(app).get("/mcp").expect(500); + }); + + it("should handle Streamable HTTP errors gracefully", async () => { + mockMCPServerEndpoint.handleStreamableHTTP.mockRejectedValueOnce( + new Error("Streamable HTTP failed") + ); + + await request(app) + .get("/mcp") + .set("mcp-session-id", "error-test") + .expect(500); + }); + }); +});