diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index e5a152de..49c24a30 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -56,11 +56,10 @@ Configuration: - FIGMA_API_KEY: ****8pXg (source: cli) - PORT: 3333 (source: default) -Initializing Figma MCP Server in HTTP mode on port 3333... +Initializing Figma MCP Server in HTTP mode on 127.0.0.1:3333... HTTP server listening on port 3333 -SSE endpoint available at http://localhost:3333/sse -Message endpoint available at http://localhost:3333/messages -New SSE connection established +StreamableHTTP endpoint available at http://127.0.0.1:3333/mcp +StreamableHTTP endpoint available at http://127.0.0.1:3333/sse (backward compat) ``` **MCP Logs** diff --git a/CLAUDE.md b/CLAUDE.md index db7b5137..f4f3f638 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -49,11 +49,10 @@ PRs are squash-merged, so the PR title becomes the commit message that release-p ### Transport Modes -The server supports three transports (all configured in `src/server.ts`): +The server supports two transports (configured in `src/server.ts`): - **stdio** — For direct MCP client integration (activated with `--stdio` flag or `NODE_ENV=cli`) -- **StreamableHTTP** — Modern HTTP transport at `/mcp` -- **SSE** — Legacy HTTP transport at `/sse` + `/messages` +- **StreamableHTTP** — Stateless HTTP transport at `/mcp` (also served at `/sse` for backward compatibility with existing client configs) ### Core Data Flow diff --git a/package.json b/package.json index 49090d7a..244b10a5 100644 --- a/package.json +++ b/package.json @@ -54,10 +54,10 @@ "cleye": "^2.2.1", "cross-env": "^7.0.3", "dotenv": "^16.4.7", - "express": "^4.21.2", + "express": "^5.2.1", + "jimp": "^1.6.0", "js-yaml": "^4.1.1", "remeda": "^2.20.1", - "jimp": "^1.6.0", "zod": "^3.25.76" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3f5085ec..3cdde305 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,8 +24,8 @@ importers: specifier: ^16.4.7 version: 16.4.7 express: - specifier: ^4.21.2 - version: 4.21.2 + specifier: ^5.2.1 + version: 5.2.1 jimp: specifier: ^1.6.0 version: 1.6.0 @@ -742,10 +742,6 @@ packages: resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} engines: {node: '>=6.5'} - accepts@1.3.8: - resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} - engines: {node: '>= 0.6'} - accepts@2.0.0: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} engines: {node: '>= 0.6'} @@ -799,9 +795,6 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - array-flatten@1.1.1: - resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} - assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} @@ -823,10 +816,6 @@ packages: bmp-ts@1.0.9: resolution: {integrity: sha512-cTEHk2jLrPyi+12M3dhpEbnnPOsaZuq7C45ylbbQIiWgDFZq4UVYPEY5mlqjvsj/6gJv9qX5sa+ebDzLXT28Vw==} - body-parser@1.20.3: - resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} - engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} - body-parser@2.2.2: resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} engines: {node: '>=18'} @@ -906,10 +895,6 @@ packages: resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} engines: {node: ^14.18.0 || >=16.10.0} - content-disposition@0.5.4: - resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} - engines: {node: '>= 0.6'} - content-disposition@1.0.0: resolution: {integrity: sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==} engines: {node: '>= 0.6'} @@ -918,9 +903,6 @@ packages: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} engines: {node: '>= 0.6'} - cookie-signature@1.0.6: - resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} - cookie-signature@1.2.2: resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} engines: {node: '>=6.6.0'} @@ -942,23 +924,6 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} - debug@2.6.9: - resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - - debug@4.4.0: - resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -975,10 +940,6 @@ packages: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} - destroy@1.2.0: - resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} - engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} - dotenv@16.4.7: resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==} engines: {node: '>=12'} @@ -999,10 +960,6 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - encodeurl@1.0.2: - resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} - engines: {node: '>= 0.8'} - encodeurl@2.0.0: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} @@ -1122,10 +1079,6 @@ packages: peerDependencies: express: '>= 4.11' - express@4.21.2: - resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} - engines: {node: '>= 0.10.0'} - express@5.2.1: resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} engines: {node: '>= 18'} @@ -1159,10 +1112,6 @@ packages: resolution: {integrity: sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw==} engines: {node: '>=10'} - finalhandler@1.3.1: - resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} - engines: {node: '>= 0.8'} - finalhandler@2.1.1: resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} engines: {node: '>= 18.0.0'} @@ -1189,10 +1138,6 @@ packages: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} - fresh@0.5.2: - resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} - engines: {node: '>= 0.6'} - fresh@2.0.0: resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} engines: {node: '>= 0.8'} @@ -1264,10 +1209,6 @@ packages: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} - iconv-lite@0.4.24: - resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} - engines: {node: '>=0.10.0'} - iconv-lite@0.6.3: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} @@ -1452,54 +1393,22 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} - media-typer@0.3.0: - resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} - engines: {node: '>= 0.6'} - media-typer@1.1.0: resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} engines: {node: '>= 0.8'} - merge-descriptors@1.0.3: - resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} - merge-descriptors@2.0.0: resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} engines: {node: '>=18'} - methods@1.1.2: - resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} - engines: {node: '>= 0.6'} - - mime-db@1.52.0: - resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} - engines: {node: '>= 0.6'} - - mime-db@1.53.0: - resolution: {integrity: sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg==} - engines: {node: '>= 0.6'} - mime-db@1.54.0: resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} engines: {node: '>= 0.6'} - mime-types@2.1.35: - resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} - engines: {node: '>= 0.6'} - - mime-types@3.0.0: - resolution: {integrity: sha512-XqoSHeCGjVClAmoGFG3lVFqQFRIrTVw2OH3axRqAcfaw+gHWIfnASS92AV+Rl/mk0MupgZTRHQOjxY6YVnzK5w==} - engines: {node: '>= 0.6'} - mime-types@3.0.2: resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} engines: {node: '>=18'} - mime@1.6.0: - resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} - engines: {node: '>=4'} - hasBin: true - mime@3.0.0: resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} engines: {node: '>=10.0.0'} @@ -1523,9 +1432,6 @@ packages: mlly@1.8.0: resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} - ms@2.0.0: - resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} - ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -1540,10 +1446,6 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - negotiator@0.6.3: - resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} - engines: {node: '>= 0.6'} - negotiator@1.0.0: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} @@ -1616,9 +1518,6 @@ packages: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} - path-to-regexp@0.1.12: - resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} - path-to-regexp@8.2.0: resolution: {integrity: sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==} engines: {node: '>=16'} @@ -1703,10 +1602,6 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} - qs@6.13.0: - resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} - engines: {node: '>=0.6'} - qs@6.15.0: resolution: {integrity: sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==} engines: {node: '>=0.6'} @@ -1715,10 +1610,6 @@ packages: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} - raw-body@2.5.2: - resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} - engines: {node: '>= 0.8'} - raw-body@3.0.0: resolution: {integrity: sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==} engines: {node: '>= 0.8'} @@ -1781,22 +1672,10 @@ packages: engines: {node: '>=10'} hasBin: true - send@0.19.0: - resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} - engines: {node: '>= 0.8.0'} - - send@1.1.0: - resolution: {integrity: sha512-v67WcEouB5GxbTWL/4NeToqcZiAWEq90N888fczVArY8A79J0L4FD7vj5hm3eUMua5EpoQ59wa/oovY6TLvRUA==} - engines: {node: '>= 18'} - send@1.2.1: resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} engines: {node: '>= 18'} - serve-static@1.16.2: - resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} - engines: {node: '>= 0.8.0'} - serve-static@2.2.1: resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} engines: {node: '>= 18'} @@ -1984,10 +1863,6 @@ packages: type-flag@4.0.3: resolution: {integrity: sha512-YA09cL07U7hSV+/doSfKl+RkIZ2olCnevZsVgAuyBUG3h2ROf9Oh2vmbq5Rf26aA9/qu9RtStuc7ap5PC6k/vw==} - type-is@1.6.18: - resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} - engines: {node: '>= 0.6'} - type-is@2.0.1: resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} engines: {node: '>= 0.6'} @@ -2016,10 +1891,6 @@ packages: utif2@4.1.0: resolution: {integrity: sha512-+oknB9FHrJ7oW7A2WZYajOcv4FcDR4CfoGB0dPNfxbi4GO05RRnFmt5oa23+9w32EanrYcSJWspUiJkLMs+37w==} - utils-merge@1.0.1: - resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} - engines: {node: '>= 0.4.0'} - vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} @@ -2813,14 +2684,9 @@ snapshots: dependencies: event-target-shim: 5.0.1 - accepts@1.3.8: - dependencies: - mime-types: 2.1.35 - negotiator: 0.6.3 - accepts@2.0.0: dependencies: - mime-types: 3.0.0 + mime-types: 3.0.2 negotiator: 1.0.0 acorn-jsx@5.3.2(acorn@8.16.0): @@ -2863,8 +2729,6 @@ snapshots: argparse@2.0.1: {} - array-flatten@1.1.1: {} - assertion-error@2.0.1: {} await-to-js@3.0.0: {} @@ -2877,29 +2741,12 @@ snapshots: bmp-ts@1.0.9: {} - body-parser@1.20.3: - dependencies: - bytes: 3.1.2 - content-type: 1.0.5 - debug: 2.6.9 - depd: 2.0.0 - destroy: 1.2.0 - http-errors: 2.0.0 - iconv-lite: 0.4.24 - on-finished: 2.4.1 - qs: 6.13.0 - raw-body: 2.5.2 - type-is: 1.6.18 - unpipe: 1.0.0 - transitivePeerDependencies: - - supports-color - body-parser@2.2.2: dependencies: bytes: 3.1.2 content-type: 1.0.5 debug: 4.4.3 - http-errors: 2.0.0 + http-errors: 2.0.1 iconv-lite: 0.7.2 on-finished: 2.4.1 qs: 6.15.0 @@ -2977,18 +2824,12 @@ snapshots: consola@3.4.2: {} - content-disposition@0.5.4: - dependencies: - safe-buffer: 5.2.1 - content-disposition@1.0.0: dependencies: safe-buffer: 5.2.1 content-type@1.0.5: {} - cookie-signature@1.0.6: {} - cookie-signature@1.2.2: {} cookie@0.7.1: {} @@ -3008,14 +2849,6 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 - debug@2.6.9: - dependencies: - ms: 2.0.0 - - debug@4.4.0: - dependencies: - ms: 2.1.3 - debug@4.4.3: dependencies: ms: 2.1.3 @@ -3024,8 +2857,6 @@ snapshots: depd@2.0.0: {} - destroy@1.2.0: {} - dotenv@16.4.7: {} dunder-proto@1.0.1: @@ -3042,8 +2873,6 @@ snapshots: emoji-regex@9.2.2: {} - encodeurl@1.0.2: {} - encodeurl@2.0.0: {} es-define-property@1.0.1: {} @@ -3186,42 +3015,6 @@ snapshots: express: 5.2.1 ip-address: 10.0.1 - express@4.21.2: - dependencies: - accepts: 1.3.8 - array-flatten: 1.1.1 - body-parser: 1.20.3 - content-disposition: 0.5.4 - content-type: 1.0.5 - cookie: 0.7.1 - cookie-signature: 1.0.6 - debug: 2.6.9 - depd: 2.0.0 - encodeurl: 2.0.0 - escape-html: 1.0.3 - etag: 1.8.1 - finalhandler: 1.3.1 - fresh: 0.5.2 - http-errors: 2.0.0 - merge-descriptors: 1.0.3 - methods: 1.1.2 - on-finished: 2.4.1 - parseurl: 1.3.3 - path-to-regexp: 0.1.12 - proxy-addr: 2.0.7 - qs: 6.13.0 - range-parser: 1.2.1 - safe-buffer: 5.2.1 - send: 0.19.0 - serve-static: 1.16.2 - setprototypeof: 1.2.0 - statuses: 2.0.1 - type-is: 1.6.18 - utils-merge: 1.0.1 - vary: 1.1.2 - transitivePeerDependencies: - - supports-color - express@5.2.1: dependencies: accepts: 2.0.0 @@ -3230,16 +3023,16 @@ snapshots: content-type: 1.0.5 cookie: 0.7.1 cookie-signature: 1.2.2 - debug: 4.4.0 + debug: 4.4.3 depd: 2.0.0 encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 finalhandler: 2.1.1 fresh: 2.0.0 - http-errors: 2.0.0 + http-errors: 2.0.1 merge-descriptors: 2.0.0 - mime-types: 3.0.0 + mime-types: 3.0.2 on-finished: 2.4.1 once: 1.4.0 parseurl: 1.3.3 @@ -3247,9 +3040,9 @@ snapshots: qs: 6.15.0 range-parser: 1.2.1 router: 2.2.0 - send: 1.1.0 + send: 1.2.1 serve-static: 2.2.1 - statuses: 2.0.1 + statuses: 2.0.2 type-is: 2.0.1 vary: 1.1.2 transitivePeerDependencies: @@ -3277,26 +3070,14 @@ snapshots: strtok3: 6.3.0 token-types: 4.2.1 - finalhandler@1.3.1: - dependencies: - debug: 2.6.9 - encodeurl: 2.0.0 - escape-html: 1.0.3 - on-finished: 2.4.1 - parseurl: 1.3.3 - statuses: 2.0.1 - unpipe: 1.0.0 - transitivePeerDependencies: - - supports-color - finalhandler@2.1.1: dependencies: - debug: 4.4.0 + debug: 4.4.3 encodeurl: 2.0.0 escape-html: 1.0.3 on-finished: 2.4.1 parseurl: 1.3.3 - statuses: 2.0.1 + statuses: 2.0.2 transitivePeerDependencies: - supports-color @@ -3325,8 +3106,6 @@ snapshots: forwarded@0.2.0: {} - fresh@0.5.2: {} - fresh@2.0.0: {} fsevents@2.3.3: @@ -3406,10 +3185,6 @@ snapshots: statuses: 2.0.2 toidentifier: 1.0.1 - iconv-lite@0.4.24: - dependencies: - safer-buffer: 2.1.2 - iconv-lite@0.6.3: dependencies: safer-buffer: 2.1.2 @@ -3581,36 +3356,16 @@ snapshots: math-intrinsics@1.1.0: {} - media-typer@0.3.0: {} - media-typer@1.1.0: {} - merge-descriptors@1.0.3: {} - merge-descriptors@2.0.0: {} - methods@1.1.2: {} - - mime-db@1.52.0: {} - - mime-db@1.53.0: {} - mime-db@1.54.0: {} - mime-types@2.1.35: - dependencies: - mime-db: 1.52.0 - - mime-types@3.0.0: - dependencies: - mime-db: 1.53.0 - mime-types@3.0.2: dependencies: mime-db: 1.54.0 - mime@1.6.0: {} - mime@3.0.0: {} minimatch@10.2.4: @@ -3634,8 +3389,6 @@ snapshots: pkg-types: 1.3.1 ufo: 1.6.3 - ms@2.0.0: {} - ms@2.1.3: {} mz@2.7.0: @@ -3648,8 +3401,6 @@ snapshots: natural-compare@1.4.0: {} - negotiator@0.6.3: {} - negotiator@1.0.0: {} object-assign@4.1.1: {} @@ -3713,8 +3464,6 @@ snapshots: lru-cache: 10.4.3 minipass: 7.1.2 - path-to-regexp@0.1.12: {} - path-to-regexp@8.2.0: {} pathe@2.0.3: {} @@ -3769,23 +3518,12 @@ snapshots: punycode@2.3.1: {} - qs@6.13.0: - dependencies: - side-channel: 1.1.0 - qs@6.15.0: dependencies: side-channel: 1.1.0 range-parser@1.2.1: {} - raw-body@2.5.2: - dependencies: - bytes: 3.1.2 - http-errors: 2.0.0 - iconv-lite: 0.4.24 - unpipe: 1.0.0 - raw-body@3.0.0: dependencies: bytes: 3.1.2 @@ -3859,7 +3597,7 @@ snapshots: router@2.2.0: dependencies: - debug: 4.4.0 + debug: 4.4.3 depd: 2.0.0 is-promise: 4.0.0 parseurl: 1.3.3 @@ -3875,41 +3613,6 @@ snapshots: semver@7.7.4: {} - send@0.19.0: - dependencies: - debug: 2.6.9 - depd: 2.0.0 - destroy: 1.2.0 - encodeurl: 1.0.2 - escape-html: 1.0.3 - etag: 1.8.1 - fresh: 0.5.2 - http-errors: 2.0.0 - mime: 1.6.0 - ms: 2.1.3 - on-finished: 2.4.1 - range-parser: 1.2.1 - statuses: 2.0.1 - transitivePeerDependencies: - - supports-color - - send@1.1.0: - dependencies: - debug: 4.4.0 - destroy: 1.2.0 - encodeurl: 2.0.0 - escape-html: 1.0.3 - etag: 1.8.1 - fresh: 0.5.2 - http-errors: 2.0.0 - mime-types: 2.1.35 - ms: 2.1.3 - on-finished: 2.4.1 - range-parser: 1.2.1 - statuses: 2.0.1 - transitivePeerDependencies: - - supports-color - send@1.2.1: dependencies: debug: 4.4.3 @@ -3926,15 +3629,6 @@ snapshots: transitivePeerDependencies: - supports-color - serve-static@1.16.2: - dependencies: - encodeurl: 2.0.0 - escape-html: 1.0.3 - parseurl: 1.3.3 - send: 0.19.0 - transitivePeerDependencies: - - supports-color - serve-static@2.2.1: dependencies: encodeurl: 2.0.0 @@ -4126,16 +3820,11 @@ snapshots: type-flag@4.0.3: {} - type-is@1.6.18: - dependencies: - media-typer: 0.3.0 - mime-types: 2.1.35 - type-is@2.0.1: dependencies: content-type: 1.0.5 media-typer: 1.1.0 - mime-types: 3.0.0 + mime-types: 3.0.2 typescript@5.7.3: {} @@ -4155,8 +3844,6 @@ snapshots: dependencies: pako: 1.0.11 - utils-merge@1.0.1: {} - vary@1.1.2: {} vite@7.3.1(@types/node@25.3.3)(tsx@4.21.0): diff --git a/src/server.ts b/src/server.ts index 9236b94a..fe2c2c36 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,23 +1,16 @@ -import { randomUUID } from "node:crypto"; -import express, { type Request, type Response } from "express"; -import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; +import { type NextFunction, type Request, type Response } from "express"; import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; -import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js"; +import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; import { Server } from "http"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { Logger } from "./utils/logger.js"; import { createServer } from "./mcp/index.js"; import { getServerConfig } from "./config.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { ErrorCode } from "@modelcontextprotocol/sdk/types.js"; let httpServer: Server | null = null; -type Session = { - transport: StreamableHTTPServerTransport | SSEServerTransport; - server: McpServer; -}; -const sessions: Record = {}; - /** * Start the MCP server in either stdio or HTTP mode. */ @@ -58,142 +51,54 @@ export async function startHttpServer( throw new Error("HTTP server is already running"); } - const app = express(); - - // Parse JSON requests for the Streamable HTTP endpoint only, will break SSE endpoint - app.use("/mcp", express.json()); + const app = createMcpExpressApp({ host }); - // Modern Streamable HTTP endpoint - app.post("/mcp", async (req, res) => { + const handlePost = async (req: Request, res: Response) => { Logger.log("Received StreamableHTTP request"); - const sessionId = req.headers["mcp-session-id"] as string | undefined; - let transport: StreamableHTTPServerTransport; - - if (sessionId && sessions[sessionId]) { - // Reuse existing transport - Logger.log("Reusing existing StreamableHTTP transport for sessionId", sessionId); - transport = sessions[sessionId].transport as StreamableHTTPServerTransport; - } else if (!sessionId && isInitializeRequest(req.body)) { - Logger.log("New initialization request for StreamableHTTP sessionId", sessionId); - transport = new StreamableHTTPServerTransport({ - sessionIdGenerator: () => randomUUID(), - onsessioninitialized: (newSessionId) => { - sessions[newSessionId] = { transport, server: mcpServer }; - }, - }); - transport.onclose = () => { - if (transport.sessionId) { - delete sessions[transport.sessionId]; - } - }; - const mcpServer = createMcpServer(); - await mcpServer.connect(transport); - } else { - // Invalid request - Logger.log("Invalid request:", req.body); - res.status(400).json({ - jsonrpc: "2.0", - error: { - code: -32000, - message: "Bad Request: No valid session ID provided", - }, - id: null, - }); - return; - } - - let progressInterval: NodeJS.Timeout | null = null; - const progressToken = req.body.params?._meta?.progressToken; - let progress = 0; - if (progressToken && sessionId && sessions[sessionId]) { - Logger.log( - `Setting up progress notifications for token ${progressToken} on session ${sessionId}`, - ); - progressInterval = setInterval(async () => { - Logger.log("Sending progress notification", progress); - await sessions[sessionId].server.server.notification({ - method: "notifications/progress", - params: { - progress, - progressToken, - }, - }); - progress++; - }, 1000); - } - - Logger.log("Handling StreamableHTTP request"); + const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined }); + const mcpServer = createMcpServer(); + res.on("close", () => { + transport.close(); + mcpServer.close(); + }); + await mcpServer.connect(transport); await transport.handleRequest(req, res, req.body); - - if (progressInterval) { - clearInterval(progressInterval); - } Logger.log("StreamableHTTP request handled"); - }); - - // Handle GET requests for SSE streams (using built-in support from StreamableHTTP) - const handleSessionRequest = async (req: Request, res: Response) => { - const sessionId = req.headers["mcp-session-id"] as string | undefined; - if (!sessionId || !sessions[sessionId]) { - res.status(400).send("Invalid or missing session ID"); - return; - } - - console.log(`Received session termination request for session ${sessionId}`); - - try { - const transport = sessions[sessionId].transport as StreamableHTTPServerTransport; - await transport.handleRequest(req, res); - } catch (error) { - console.error("Error handling session termination:", error); - if (!res.headersSent) { - res.status(500).send("Error processing session termination"); - } - } }; - // Handle GET requests for server-to-client notifications via SSE - app.get("/mcp", handleSessionRequest); - - // Handle DELETE requests for session termination - app.delete("/mcp", handleSessionRequest); - - app.get("/sse", async (req, res) => { - Logger.log("Establishing new SSE connection"); - const transport = new SSEServerTransport("/messages", res); - Logger.log(`New SSE connection established for sessionId ${transport.sessionId}`); - Logger.log("/sse request headers:", req.headers); - Logger.log("/sse request body:", req.body); - - const mcpServer = createMcpServer(); - sessions[transport.sessionId] = { transport, server: mcpServer }; - res.on("close", () => { - delete sessions[transport.sessionId]; - }); + const handleMethodNotAllowed = (_req: Request, res: Response) => { + res.status(405).set("Allow", "POST").send("Method Not Allowed"); + }; - await mcpServer.connect(transport); - }); + // Mount stateless StreamableHTTP on both /mcp and /sse. + // Serving StreamableHTTP at /sse lets existing client configs keep working — + // modern MCP clients probe with a POST before falling back to SSE. + for (const path of ["/mcp", "/sse"]) { + app.post(path, handlePost); + app.get(path, handleMethodNotAllowed); + app.delete(path, handleMethodNotAllowed); + } - app.post("/messages", async (req, res) => { - const sessionId = req.query.sessionId as string; - const session = sessions[sessionId]; - if (session) { - Logger.log(`Received SSE message for sessionId ${sessionId}`); - Logger.log("/messages request headers:", req.headers); - Logger.log("/messages request body:", req.body); - await (session.transport as SSEServerTransport).handlePostMessage(req, res); - } else { - res.status(400).send(`No transport found for sessionId ${sessionId}`); - return; + // Express 5 forwards rejected promises from async handlers here. + // Return a JSON-RPC error instead of Express's default HTML 500. + app.use((err: unknown, _req: Request, res: Response, _next: NextFunction) => { + Logger.log("Unhandled error:", err); + if (!res.headersSent) { + res.status(500).json({ + jsonrpc: "2.0", + error: { code: ErrorCode.InternalError, message: "Internal server error" }, + id: null, + }); } }); return new Promise((resolve, reject) => { const server = app.listen(port, host, () => { Logger.log(`HTTP server listening on port ${port}`); - Logger.log(`SSE endpoint available at http://${host}:${port}/sse`); - Logger.log(`Message endpoint available at http://${host}:${port}/messages`); Logger.log(`StreamableHTTP endpoint available at http://${host}:${port}/mcp`); + Logger.log( + `StreamableHTTP endpoint available at http://${host}:${port}/sse (backward compat)`, + ); resolve(server); }); server.once("error", (err) => { @@ -209,17 +114,6 @@ export async function stopHttpServer(): Promise { throw new Error("HTTP server is not running"); } - // Close all sessions FIRST so connections drain - for (const sessionId in sessions) { - try { - await sessions[sessionId].transport.close(); - delete sessions[sessionId]; - } catch (error) { - console.error(`Error closing session ${sessionId}:`, error); - } - } - - // Then close the HTTP server return new Promise((resolve, reject) => { httpServer!.close((err) => { httpServer = null; diff --git a/src/tests/server.test.ts b/src/tests/server.test.ts index 9f884651..8f72c132 100644 --- a/src/tests/server.test.ts +++ b/src/tests/server.test.ts @@ -1,6 +1,5 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; -import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; import { createServer } from "../mcp/index.js"; import { startHttpServer, stopHttpServer } from "../server.js"; import type { AddressInfo } from "net"; @@ -16,8 +15,9 @@ describe("StreamableHTTP transport", () => { let port: number; beforeAll(async () => { - const mcpServer = createServer(dummyAuth, { isHTTP: true }); - const httpServer = await startHttpServer("127.0.0.1", 0, () => mcpServer); + const httpServer = await startHttpServer("127.0.0.1", 0, () => + createServer(dummyAuth, { isHTTP: true }), + ); port = (httpServer.address() as AddressInfo).port; }, 15_000); @@ -29,7 +29,7 @@ describe("StreamableHTTP transport", () => { } }); - it("connects, initializes, and lists tools", async () => { + it("connects, initializes, and lists tools via /mcp", async () => { const client = new Client({ name: "test-streamable", version: "1.0.0" }); const transport = new StreamableHTTPClientTransport(new URL(`http://127.0.0.1:${port}/mcp`)); @@ -41,31 +41,12 @@ describe("StreamableHTTP transport", () => { expect(toolNames).toContain("get_figma_data"); expect(toolNames).toContain("download_figma_images"); - await transport.terminateSession(); await client.close(); }, 15_000); -}); - -describe("SSE transport", () => { - let port: number; - - beforeAll(async () => { - const mcpServer = createServer(dummyAuth, { isHTTP: true }); - const httpServer = await startHttpServer("127.0.0.1", 0, () => mcpServer); - port = (httpServer.address() as AddressInfo).port; - }, 15_000); - - afterAll(async () => { - try { - await stopHttpServer(); - } catch { - // Server may not have started - } - }); - it("connects, initializes, and lists tools", async () => { - const client = new Client({ name: "test-sse", version: "1.0.0" }); - const transport = new SSEClientTransport(new URL(`http://127.0.0.1:${port}/sse`)); + it("connects, initializes, and lists tools via /sse (backward compat)", async () => { + const client = new Client({ name: "test-sse-compat", version: "1.0.0" }); + const transport = new StreamableHTTPClientTransport(new URL(`http://127.0.0.1:${port}/sse`)); await client.connect(transport); @@ -73,72 +54,34 @@ describe("SSE transport", () => { const toolNames = tools.map((t) => t.name); expect(toolNames).toContain("get_figma_data"); + expect(toolNames).toContain("download_figma_images"); await client.close(); }, 15_000); -}); - -describe("Negative protocol tests", () => { - let port: number; - - beforeAll(async () => { - const mcpServer = createServer(dummyAuth, { isHTTP: true }); - const httpServer = await startHttpServer("127.0.0.1", 0, () => mcpServer); - port = (httpServer.address() as AddressInfo).port; - }, 15_000); - - afterAll(async () => { - try { - await stopHttpServer(); - } catch { - // Server may not have started - } - }); - - it("POST /mcp without session ID and non-initialize body returns 400", async () => { - const res = await fetch(`http://127.0.0.1:${port}/mcp`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - jsonrpc: "2.0", - method: "tools/list", - id: 1, - }), - }); - expect(res.status).toBe(400); - }); - - it("GET /mcp with invalid session ID returns 400", async () => { - const res = await fetch(`http://127.0.0.1:${port}/mcp`, { - method: "GET", - headers: { "mcp-session-id": "nonexistent-session" }, - }); - expect(res.status).toBe(400); - }); - it("DELETE /mcp with invalid session ID returns 400", async () => { + it("responses contain no mcp-session-id header", async () => { const res = await fetch(`http://127.0.0.1:${port}/mcp`, { - method: "DELETE", - headers: { "mcp-session-id": "nonexistent-session" }, - }); - expect(res.status).toBe(400); - }); - - it("POST /messages with unknown sessionId returns 400", async () => { - const res = await fetch(`http://127.0.0.1:${port}/messages?sessionId=nonexistent`, { method: "POST", - headers: { "Content-Type": "application/json" }, + headers: { + "Content-Type": "application/json", + Accept: "application/json, text/event-stream", + }, body: JSON.stringify({ jsonrpc: "2.0", - method: "tools/list", + method: "initialize", + params: { + protocolVersion: "2025-03-26", + capabilities: {}, + clientInfo: { name: "test", version: "1.0.0" }, + }, id: 1, }), }); - expect(res.status).toBe(400); - }); + expect(res.headers.get("mcp-session-id")).toBeNull(); + }, 15_000); }); -describe("Multi-client test", () => { +describe("Method not allowed", () => { let port: number; beforeAll(async () => { @@ -156,37 +99,28 @@ describe("Multi-client test", () => { } }); - it("StreamableHTTP and SSE clients work concurrently", async () => { - const streamableClient = new Client({ name: "test-streamable", version: "1.0.0" }); - const streamableTransport = new StreamableHTTPClientTransport( - new URL(`http://127.0.0.1:${port}/mcp`), - ); - - const sseClient = new Client({ name: "test-sse", version: "1.0.0" }); - const sseTransport = new SSEClientTransport(new URL(`http://127.0.0.1:${port}/sse`)); - - // Connect both concurrently - await Promise.all([ - streamableClient.connect(streamableTransport), - sseClient.connect(sseTransport), - ]); + it("GET /mcp returns 405", async () => { + const res = await fetch(`http://127.0.0.1:${port}/mcp`, { method: "GET" }); + expect(res.status).toBe(405); + }); - // Both should be able to list tools - const [streamableTools, sseTools] = await Promise.all([ - streamableClient.listTools(), - sseClient.listTools(), - ]); + it("DELETE /mcp returns 405", async () => { + const res = await fetch(`http://127.0.0.1:${port}/mcp`, { method: "DELETE" }); + expect(res.status).toBe(405); + }); - expect(streamableTools.tools.map((t) => t.name)).toContain("get_figma_data"); - expect(sseTools.tools.map((t) => t.name)).toContain("get_figma_data"); + it("GET /sse returns 405", async () => { + const res = await fetch(`http://127.0.0.1:${port}/sse`, { method: "GET" }); + expect(res.status).toBe(405); + }); - // Clean up - await streamableTransport.terminateSession(); - await Promise.all([streamableClient.close(), sseClient.close()]); - }, 15_000); + it("DELETE /sse returns 405", async () => { + const res = await fetch(`http://127.0.0.1:${port}/sse`, { method: "DELETE" }); + expect(res.status).toBe(405); + }); }); -describe("Session reconnection", () => { +describe("Multi-client test", () => { let port: number; beforeAll(async () => { @@ -204,54 +138,29 @@ describe("Session reconnection", () => { } }); - it("connects, terminates, and reconnects successfully", async () => { - // First session - const client1 = new Client({ name: "test-reconnect-1", version: "1.0.0" }); - const transport1 = new StreamableHTTPClientTransport(new URL(`http://127.0.0.1:${port}/mcp`)); - await client1.connect(transport1); - const { tools: tools1 } = await client1.listTools(); - expect(tools1.map((t) => t.name)).toContain("get_figma_data"); - - await transport1.terminateSession(); - await client1.close(); - - // Second session after termination - const client2 = new Client({ name: "test-reconnect-2", version: "1.0.0" }); - const transport2 = new StreamableHTTPClientTransport(new URL(`http://127.0.0.1:${port}/mcp`)); - await client2.connect(transport2); - const { tools: tools2 } = await client2.listTools(); - expect(tools2.map((t) => t.name)).toContain("get_figma_data"); - - await transport2.terminateSession(); - await client2.close(); - }, 15_000); + it("multiple StreamableHTTP clients work concurrently", async () => { + const clientA = new Client({ name: "test-a", version: "1.0.0" }); + const transportA = new StreamableHTTPClientTransport(new URL(`http://127.0.0.1:${port}/mcp`)); + + const clientB = new Client({ name: "test-b", version: "1.0.0" }); + const transportB = new StreamableHTTPClientTransport(new URL(`http://127.0.0.1:${port}/sse`)); + + await Promise.all([clientA.connect(transportA), clientB.connect(transportB)]); + + const [toolsA, toolsB] = await Promise.all([clientA.listTools(), clientB.listTools()]); - it("reconnects after client drops without clean termination", async () => { - // First session — close abruptly without terminateSession() - const client1 = new Client({ name: "test-dirty-close-1", version: "1.0.0" }); - const transport1 = new StreamableHTTPClientTransport(new URL(`http://127.0.0.1:${port}/mcp`)); - await client1.connect(transport1); - await client1.listTools(); - - // Simulate unclean disconnect (just close, no terminate) - await client1.close(); - - // Second session should still work - const client2 = new Client({ name: "test-dirty-close-2", version: "1.0.0" }); - const transport2 = new StreamableHTTPClientTransport(new URL(`http://127.0.0.1:${port}/mcp`)); - await client2.connect(transport2); - const { tools } = await client2.listTools(); - expect(tools.map((t) => t.name)).toContain("get_figma_data"); - - await transport2.terminateSession(); - await client2.close(); + expect(toolsA.tools.map((t) => t.name)).toContain("get_figma_data"); + expect(toolsB.tools.map((t) => t.name)).toContain("get_figma_data"); + + await Promise.all([clientA.close(), clientB.close()]); }, 15_000); }); describe("Server lifecycle", () => { it("starts and listens on assigned port", async () => { - const mcpServer = createServer(dummyAuth, { isHTTP: true }); - const httpServer = await startHttpServer("127.0.0.1", 0, () => mcpServer); + const httpServer = await startHttpServer("127.0.0.1", 0, () => + createServer(dummyAuth, { isHTTP: true }), + ); const port = (httpServer.address() as AddressInfo).port; expect(port).toBeGreaterThan(0); @@ -260,10 +169,8 @@ describe("Server lifecycle", () => { }, 15_000); it("stopHttpServer shuts down cleanly without hanging", async () => { - const mcpServer = createServer(dummyAuth, { isHTTP: true }); - await startHttpServer("127.0.0.1", 0, () => mcpServer); + await startHttpServer("127.0.0.1", 0, () => createServer(dummyAuth, { isHTTP: true })); - // Race stopHttpServer against a deadline const timeout = new Promise<"timeout">((resolve) => setTimeout(() => resolve("timeout"), 5_000).unref(), );