From a2a4ef31566eceacf7a5afbea35c6269c0a37470 Mon Sep 17 00:00:00 2001 From: Andrew Qu Date: Wed, 29 Jul 2026 17:03:03 -0700 Subject: [PATCH] test: harden MCP v2 upgrade boundaries --- .changeset/static-route-handler.md | 7 + README.md | 5 + docs/ADVANCED.md | 22 +- package.json | 10 +- pnpm-lock.yaml | 682 +++++++++++++++++++++++++++++ src/cli/index.ts | 38 +- src/handler/mcp-api-handler.ts | 92 ++-- tests/auth-wrapper.test.ts | 102 +++++ tests/cli.test.ts | 63 +++ tests/e2e.test.ts | 87 +++- tests/handler.test.ts | 257 +++++++++++ tests/package.test.ts | 81 ++++ 12 files changed, 1405 insertions(+), 41 deletions(-) create mode 100644 .changeset/static-route-handler.md create mode 100644 tests/auth-wrapper.test.ts create mode 100644 tests/cli.test.ts create mode 100644 tests/handler.test.ts create mode 100644 tests/package.test.ts diff --git a/.changeset/static-route-handler.md b/.changeset/static-route-handler.md new file mode 100644 index 0000000..0ca631d --- /dev/null +++ b/.changeset/static-route-handler.md @@ -0,0 +1,7 @@ +--- +"mcp-handler": patch +--- + +Reject browser requests from unexpected Origin hostnames by default, add `allowedOriginHostnames` for explicit cross-origin access, preserve the HTTP `Allow: POST` header on stateless `405` responses, and isolate synchronous or asynchronous `onEvent` callback failures from MCP requests. + +The CLI now supports `src/app`, `--no-install`, and refuses to overwrite an existing route. The published package now includes the documentation linked from the npm README and points its CommonJS declaration export at the generated `dist/index.d.ts` file. diff --git a/README.md b/README.md index 23fe06e..e31d488 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,10 @@ export { handler as GET, handler as POST }; the request pathname, so you can place this handler at any framework route and give clients that route's complete URL. +Browser requests with an `Origin` header are restricted to the MCP server's +public hostname by default. See [Advanced Usage](docs/ADVANCED.md) to allow +additional browser Origin hostnames. + ## Connecting Clients If your client supports Streamable HTTP, connect directly: @@ -114,6 +118,7 @@ See [Authorization](docs/AUTHORIZATION.md) for wiring details. - **Framework-agnostic**: Web-standard `Request` and `Response` handler - **Framework compatibility**: Next.js, Nuxt/Nitro, SvelteKit, Hono, and other Fetch-compatible frameworks - **Dual-era protocol support**: 2026-07-28 (stateless) and 2025-era Streamable HTTP from one handler +- **Secure browser boundary**: Origin validation with an explicit cross-origin allowlist - **TypeScript Support**: Full type definitions included ## Requirements diff --git a/docs/ADVANCED.md b/docs/ADVANCED.md index f8418bb..cab8cbe 100644 --- a/docs/ADVANCED.md +++ b/docs/ADVANCED.md @@ -44,10 +44,30 @@ export { handler as GET, handler as POST }; ```typescript interface Config { verboseLogs?: boolean; // Enable debug logging - onEvent?: (event: McpEvent) => void; // Analytics/debugging callback + onEvent?: (event: McpEvent) => void | Promise; // Analytics/debugging callback + allowedOriginHostnames?: string[]; // Additional browser Origin hostnames } ``` +Requests with an `Origin` header are accepted when the Origin hostname matches +the public MCP server hostname. Add hostname-only entries (without a scheme or +port) to `allowedOriginHostnames` when a browser-based client is hosted on a +different hostname: + +```typescript +const handler = createMcpHandler( + initializeServer, + {}, + { + allowedOriginHostnames: ["inspector.example.com"], + }, +); +``` + +Server-side MCP clients normally omit `Origin` and are unaffected. Event +callbacks may be synchronous or asynchronous; callback failures are logged +when `verboseLogs` is enabled and never fail the MCP request. + ## Nuxt Usage ```typescript diff --git a/package.json b/package.json index 3ab68ff..622caa9 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ ".": { "types": { "import": "./dist/index.d.mts", - "require": "./dist/index.d.cjs" + "require": "./dist/index.d.ts" }, "import": "./dist/index.mjs", "require": "./dist/index.js" @@ -22,14 +22,15 @@ "./next": { "types": { "import": "./dist/index.d.mts", - "require": "./dist/index.d.cjs" + "require": "./dist/index.d.ts" }, "import": "./dist/index.mjs", "require": "./dist/index.js" } }, "files": [ - "dist" + "dist", + "docs" ], "repository": { "type": "git", @@ -41,7 +42,7 @@ "scripts": { "build": "tsup", "dev": "tsup --watch", - "test": "vitest run", + "test": "pnpm build && vitest run", "test:watch": "vitest watch", "version-packages": "changeset version", "changeset": "changeset", @@ -63,6 +64,7 @@ "@changesets/cli": "^2.27.12", "@modelcontextprotocol/client": "^2.0.0", "@modelcontextprotocol/server": "^2.0.0", + "@modelcontextprotocol/sdk-v1": "npm:@modelcontextprotocol/sdk@1.26.0", "@types/node": "^22.15.8", "tsup": "^8.0.0", "typescript": "^5.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ec5b3b5..21cb61b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,6 +24,9 @@ importers: '@modelcontextprotocol/client': specifier: ^2.0.0 version: 2.0.0 + '@modelcontextprotocol/sdk-v1': + specifier: npm:@modelcontextprotocol/sdk@1.26.0 + version: '@modelcontextprotocol/sdk@1.26.0(zod@4.4.3)' '@modelcontextprotocol/server': specifier: ^2.0.0 version: 2.0.0 @@ -392,6 +395,12 @@ packages: cpu: [x64] os: [win32] + '@hono/node-server@1.19.17': + resolution: {integrity: sha512-dSneS5qhiauZWGDCeK4o695Xd9nUNjviSZCMQrj10eetr8Uln1ucn6bbphOM6UynAMMtNIzZNSpL9vnASJwrPQ==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -428,6 +437,16 @@ packages: resolution: {integrity: sha512-pJCEwGG7Lfr/+PQp9ZTwKXNeO5wzbfKL7H3MYpCorM4oFBoQrdjnBgEoqG+RjhsvS1FKrDbKux+M1HhlnGWqcA==} engines: {node: '>=20'} + '@modelcontextprotocol/sdk@1.26.0': + resolution: {integrity: sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==} + engines: {node: '>=18'} + peerDependencies: + '@cfworker/json-schema': ^4.1.1 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + '@cfworker/json-schema': + optional: true + '@modelcontextprotocol/server@2.0.0': resolution: {integrity: sha512-YhHWdHfpFMQfd0prsEnxKeS3Qz3ytIGmsS0sth4KDjnacIT7hxk6hXHkJ9KysxlkvTM+WZAtQbbcUhdoP4Hvtw==} engines: {node: '>=20'} @@ -676,6 +695,21 @@ packages: '@vitest/utils@3.2.1': resolution: {integrity: sha512-KkHlGhePEKZSub5ViknBcN5KEF+u7dSUr9NW8QsVICusUojrgrOnnY3DEWWO877ax2Pyopuk2qHmt+gkNKnBVw==} + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + ansi-colors@4.1.3: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} engines: {node: '>=6'} @@ -725,6 +759,10 @@ packages: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} + body-parser@2.3.0: + resolution: {integrity: sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==} + engines: {node: '>=18'} + brace-expansion@2.0.1: resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} @@ -738,10 +776,22 @@ packages: peerDependencies: esbuild: '>=0.17' + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + caniuse-lite@1.0.30001718: resolution: {integrity: sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==} @@ -786,6 +836,30 @@ packages: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} + content-disposition@1.1.0: + resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==} + engines: {node: '>=18'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + content-type@2.0.0: + resolution: {integrity: sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==} + engines: {node: '>=18'} + + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} + engines: {node: '>= 0.10'} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -799,10 +873,23 @@ packages: supports-color: optional: true + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + deep-eql@5.0.2: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + detect-indent@6.1.0: resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} engines: {node: '>=8'} @@ -811,22 +898,45 @@ packages: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + enquirer@2.4.1: resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} engines: {node: '>=8.6'} + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es-object-atoms@1.1.2: + resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} + engines: {node: '>= 0.4'} + esbuild@0.19.12: resolution: {integrity: sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==} engines: {node: '>=12'} @@ -837,6 +947,9 @@ packages: engines: {node: '>=18'} hasBin: true + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + esprima@4.0.1: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} engines: {node: '>=4'} @@ -845,6 +958,10 @@ packages: estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + eventsource-parser@3.0.2: resolution: {integrity: sha512-6RxOBZ/cYgd8usLwsEl+EC09Au/9BcmCKYF2/xbml6DNczf7nv0MQb+7BA2F+li6//I+28VNlQR37XfQtcAJuA==} engines: {node: '>=18.0.0'} @@ -861,6 +978,16 @@ packages: resolution: {integrity: sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==} engines: {node: '>=12.0.0'} + express-rate-limit@8.6.1: + resolution: {integrity: sha512-0D493aP61w0TJ2A0wy27riRsO7FMQ7FK+KUHOKCSfPvYo0R55aiC6emCVgFUeShH0fq0ICPVzNcgoS+BsbXQCA==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + extendable-error@0.1.7: resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==} @@ -868,10 +995,16 @@ packages: resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} engines: {node: '>=4'} + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-glob@3.3.3: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} + fast-uri@3.1.4: + resolution: {integrity: sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==} + fastq@1.19.1: resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} @@ -887,6 +1020,10 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + find-up@4.1.0: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} @@ -895,6 +1032,14 @@ packages: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + fs-extra@7.0.1: resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} engines: {node: '>=6 <7 || >=8'} @@ -908,6 +1053,17 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + get-stream@6.0.1: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} engines: {node: '>=10'} @@ -918,15 +1074,36 @@ packages: glob@10.4.5: resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true globby@11.1.0: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} engines: {node: '>=10'} + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + + hono@4.12.32: + resolution: {integrity: sha512-XcuyW9qE2kJn07PkecMOBd5Vq/hMy7mmGw+idz1yblbg9N17ijJODrvPkn7/dwL3Kulj8LcRJ69DLOWf91dRUg==} + engines: {node: '>=16.9.0'} + + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + human-id@4.1.1: resolution: {integrity: sha512-3gKm/gCSUipeLsRYZbbdA1BD83lBoWUkZ7G9VFrhWPAU76KwYo5KR8V28bpoPm/ygy0x5/GCbpRQdY7VLYCoIg==} hasBin: true @@ -939,10 +1116,25 @@ packages: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} + iconv-lite@0.7.3: + resolution: {integrity: sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==} + engines: {node: '>=0.10.0'} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ip-address@10.3.1: + resolution: {integrity: sha512-1e9d3kb97NHJTIJDZW9rKqW2h6+dFa50Dy0fpPSMQp2ADje5gvKsXmdiK6dwY5t76TaTt5+P5N1Y/LoToIxP6g==} + engines: {node: '>= 12'} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + is-binary-path@2.1.0: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} engines: {node: '>=8'} @@ -963,6 +1155,9 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + is-stream@2.0.1: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} @@ -995,6 +1190,12 @@ packages: resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} hasBin: true + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-schema-typed@8.0.2: + resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} + jsonfile@4.0.0: resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} @@ -1032,6 +1233,18 @@ packages: magic-string@0.30.17: resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + media-typer@1.1.1: + resolution: {integrity: sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==} + engines: {node: '>= 0.8'} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -1043,6 +1256,14 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + mimic-fn@2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} @@ -1070,6 +1291,10 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + next@13.0.0: resolution: {integrity: sha512-puH1WGM6rGeFOoFdXXYfUxN9Sgi4LMytCV5HkQJvVUOhHfC1DoVqOfvzaEteyp6P04IW+gbtK2Q9pInVSrltPA==} engines: {node: '>=14.6.0'} @@ -1100,6 +1325,17 @@ packages: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + onetime@5.1.2: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} @@ -1137,6 +1373,10 @@ packages: package-manager-detector@0.2.11: resolution: {integrity: sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==} + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -1149,6 +1389,9 @@ packages: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} + path-to-regexp@8.4.2: + resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} + path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} @@ -1208,16 +1451,32 @@ packages: engines: {node: '>=10.13.0'} hasBin: true + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + qs@6.15.3: + resolution: {integrity: sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==} + engines: {node: '>=0.6'} + quansync@0.2.10: resolution: {integrity: sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A==} queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + range-parser@1.3.0: + resolution: {integrity: sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==} + engines: {node: '>= 0.6'} + + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + react-dom@18.3.1: resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} peerDependencies: @@ -1235,6 +1494,10 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + resolve-from@5.0.0: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} @@ -1248,6 +1511,10 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -1262,6 +1529,17 @@ packages: engines: {node: '>=10'} hasBin: true + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -1270,6 +1548,22 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.1: + resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} + engines: {node: '>= 0.4'} + siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} @@ -1291,6 +1585,7 @@ packages: source-map@0.8.0-beta.0: resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} engines: {node: '>= 8'} + deprecated: The work that was done in this beta branch won't be included in future versions spawndamnit@3.0.1: resolution: {integrity: sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg==} @@ -1301,6 +1596,10 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + std-env@3.9.0: resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==} @@ -1387,6 +1686,10 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + tr46@1.0.1: resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} @@ -1419,6 +1722,10 @@ packages: typescript: optional: true + type-is@2.1.0: + resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==} + engines: {node: '>= 18'} + typescript@5.0.2: resolution: {integrity: sha512-wVORMBGO/FAs/++blGNeAVdbNKtIh1rbBL2EyQ1+J9lClJ93KiiKe8PmFIVdXhHcyv44SL9oglmfeSsndo0jRw==} engines: {node: '>=12.20'} @@ -1431,11 +1738,19 @@ packages: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} engines: {node: '>= 4.0.0'} + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + use-sync-external-store@1.2.0: resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + vite-node@3.2.1: resolution: {integrity: sha512-V4EyKQPxquurNJPtQJRZo8hKOoKNBRIhxcDbQFPFig0JdoWcUhwRgK8yoCXXrfYVPKS6XwirGHPszLnR8FbjCA==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -1533,11 +1848,19 @@ packages: resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} engines: {node: '>=12'} + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + yaml@2.8.0: resolution: {integrity: sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==} engines: {node: '>= 14.6'} hasBin: true + zod-to-json-schema@3.25.2: + resolution: {integrity: sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==} + peerDependencies: + zod: ^3.25.28 || ^4 + zod@4.4.3: resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} @@ -1831,6 +2154,10 @@ snapshots: '@esbuild/win32-x64@0.25.5': optional: true + '@hono/node-server@1.19.17(hono@4.12.32)': + dependencies: + hono: 4.12.32 + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -1887,6 +2214,28 @@ snapshots: dependencies: zod: 4.4.3 + '@modelcontextprotocol/sdk@1.26.0(zod@4.4.3)': + dependencies: + '@hono/node-server': 1.19.17(hono@4.12.32) + ajv: 8.20.0 + ajv-formats: 3.0.1(ajv@8.20.0) + content-type: 1.0.5 + cors: 2.8.6 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.0.2 + express: 5.2.1 + express-rate-limit: 8.6.1(express@5.2.1) + hono: 4.12.32 + jose: 6.1.3 + json-schema-typed: 8.0.2 + pkce-challenge: 5.0.0 + raw-body: 3.0.2 + zod: 4.4.3 + zod-to-json-schema: 3.25.2(zod@4.4.3) + transitivePeerDependencies: + - supports-color + '@modelcontextprotocol/server@2.0.0': dependencies: '@modelcontextprotocol/core': 2.0.0 @@ -2067,6 +2416,22 @@ snapshots: loupe: 3.1.3 tinyrainbow: 2.0.0 + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.0.0 + + ajv-formats@3.0.1(ajv@8.20.0): + optionalDependencies: + ajv: 8.20.0 + + ajv@8.20.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.4 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + ansi-colors@4.1.3: {} ansi-regex@5.0.1: {} @@ -2102,6 +2467,20 @@ snapshots: binary-extensions@2.3.0: {} + body-parser@2.3.0: + dependencies: + bytes: 3.1.2 + content-type: 2.0.0 + debug: 4.4.3 + http-errors: 2.0.1 + iconv-lite: 0.7.3 + on-finished: 2.4.1 + qs: 6.15.3 + raw-body: 3.0.2 + type-is: 2.1.0 + transitivePeerDependencies: + - supports-color + brace-expansion@2.0.1: dependencies: balanced-match: 1.0.2 @@ -2115,8 +2494,20 @@ snapshots: esbuild: 0.19.12 load-tsconfig: 0.2.5 + bytes@3.1.2: {} + cac@6.7.14: {} + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + caniuse-lite@1.0.30001718: {} chai@5.2.0: @@ -2159,6 +2550,21 @@ snapshots: commander@4.1.1: {} + content-disposition@1.1.0: {} + + content-type@1.0.5: {} + + content-type@2.0.0: {} + + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + + cors@2.8.6: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -2169,27 +2575,51 @@ snapshots: dependencies: ms: 2.1.3 + debug@4.4.3: + dependencies: + ms: 2.1.3 + deep-eql@5.0.2: {} + depd@2.0.0: {} + detect-indent@6.1.0: {} dir-glob@3.0.1: dependencies: path-type: 4.0.0 + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + eastasianwidth@0.2.0: {} + ee-first@1.1.1: {} + emoji-regex@8.0.0: {} emoji-regex@9.2.2: {} + encodeurl@2.0.0: {} + enquirer@2.4.1: dependencies: ansi-colors: 4.1.3 strip-ansi: 6.0.1 + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + es-module-lexer@1.7.0: {} + es-object-atoms@1.1.2: + dependencies: + es-errors: 1.3.0 + esbuild@0.19.12: optionalDependencies: '@esbuild/aix-ppc64': 0.19.12 @@ -2244,12 +2674,16 @@ snapshots: '@esbuild/win32-ia32': 0.25.5 '@esbuild/win32-x64': 0.25.5 + escape-html@1.0.3: {} + esprima@4.0.1: {} estree-walker@3.0.3: dependencies: '@types/estree': 1.0.7 + etag@1.8.1: {} + eventsource-parser@3.0.2: {} eventsource@3.0.7: @@ -2270,6 +2704,47 @@ snapshots: expect-type@1.2.1: {} + express-rate-limit@8.6.1(express@5.2.1): + dependencies: + debug: 4.4.3 + express: 5.2.1 + ip-address: 10.3.1 + transitivePeerDependencies: + - supports-color + + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.3.0 + content-disposition: 1.1.0 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.1 + 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.1 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.15.3 + range-parser: 1.3.0 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.2 + type-is: 2.1.0 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + extendable-error@0.1.7: {} external-editor@3.1.0: @@ -2278,6 +2753,8 @@ snapshots: iconv-lite: 0.4.24 tmp: 0.0.33 + fast-deep-equal@3.1.3: {} + fast-glob@3.3.3: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -2286,6 +2763,8 @@ snapshots: merge2: 1.4.1 micromatch: 4.0.8 + fast-uri@3.1.4: {} + fastq@1.19.1: dependencies: reusify: 1.1.0 @@ -2298,6 +2777,17 @@ snapshots: dependencies: to-regex-range: 5.0.1 + finalhandler@2.1.1: + dependencies: + debug: 4.4.1 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + find-up@4.1.0: dependencies: locate-path: 5.0.0 @@ -2308,6 +2798,10 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 + forwarded@0.2.0: {} + + fresh@2.0.0: {} + fs-extra@7.0.1: dependencies: graceful-fs: 4.2.11 @@ -2323,6 +2817,26 @@ snapshots: fsevents@2.3.3: optional: true + function-bind@1.1.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.4 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.2 + get-stream@6.0.1: {} glob-parent@5.1.2: @@ -2347,8 +2861,26 @@ snapshots: merge2: 1.4.1 slash: 3.0.0 + gopd@1.2.0: {} + graceful-fs@4.2.11: {} + has-symbols@1.1.0: {} + + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + + hono@4.12.32: {} + + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + human-id@4.1.1: {} human-signals@2.1.0: {} @@ -2357,8 +2889,18 @@ snapshots: dependencies: safer-buffer: 2.1.2 + iconv-lite@0.7.3: + dependencies: + safer-buffer: 2.1.2 + ignore@5.3.2: {} + inherits@2.0.4: {} + + ip-address@10.3.1: {} + + ipaddr.js@1.9.1: {} + is-binary-path@2.1.0: dependencies: binary-extensions: 2.3.0 @@ -2373,6 +2915,8 @@ snapshots: is-number@7.0.0: {} + is-promise@4.0.0: {} + is-stream@2.0.1: {} is-subdir@1.2.0: @@ -2400,6 +2944,10 @@ snapshots: argparse: 1.0.10 esprima: 4.0.1 + json-schema-traverse@1.0.0: {} + + json-schema-typed@8.0.2: {} + jsonfile@4.0.0: optionalDependencies: graceful-fs: 4.2.11 @@ -2430,6 +2978,12 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 + math-intrinsics@1.1.0: {} + + media-typer@1.1.1: {} + + merge-descriptors@2.0.0: {} + merge-stream@2.0.0: {} merge2@1.4.1: {} @@ -2439,6 +2993,12 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 + mime-db@1.54.0: {} + + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + mimic-fn@2.1.0: {} minimatch@9.0.5: @@ -2459,6 +3019,8 @@ snapshots: nanoid@3.3.11: {} + negotiator@1.0.0: {} + next@13.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@next/env': 13.0.0 @@ -2495,6 +3057,16 @@ snapshots: object-assign@4.1.1: {} + object-inspect@1.13.4: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + onetime@5.1.2: dependencies: mimic-fn: 2.1.0 @@ -2525,6 +3097,8 @@ snapshots: dependencies: quansync: 0.2.10 + parseurl@1.3.3: {} + path-exists@4.0.0: {} path-key@3.1.1: {} @@ -2534,6 +3108,8 @@ snapshots: lru-cache: 10.4.3 minipass: 7.1.2 + path-to-regexp@8.4.2: {} + path-type@4.0.0: {} pathe@2.0.3: {} @@ -2573,12 +3149,31 @@ snapshots: prettier@2.8.8: {} + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + punycode@2.3.1: {} + qs@6.15.3: + dependencies: + es-define-property: 1.0.1 + side-channel: 1.1.1 + quansync@0.2.10: {} queue-microtask@1.2.3: {} + range-parser@1.3.0: {} + + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.3 + unpipe: 1.0.0 + react-dom@18.3.1(react@18.3.1): dependencies: loose-envify: 1.4.0 @@ -2600,6 +3195,8 @@ snapshots: dependencies: picomatch: 2.3.1 + require-from-string@2.0.2: {} + resolve-from@5.0.0: {} reusify@1.1.0: {} @@ -2630,6 +3227,16 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.40.2 fsevents: 2.3.3 + router@2.2.0: + dependencies: + debug: 4.4.1 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.4.2 + transitivePeerDependencies: + - supports-color + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -2642,12 +3249,67 @@ snapshots: semver@7.7.2: {} + send@1.2.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.3.0 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + + setprototypeof@1.2.0: {} + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 shebang-regex@3.0.0: {} + side-channel-list@1.0.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.1 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} signal-exit@3.0.7: {} @@ -2671,6 +3333,8 @@ snapshots: stackback@0.0.2: {} + statuses@2.0.2: {} + std-env@3.9.0: {} string-width@4.2.3: @@ -2745,6 +3409,8 @@ snapshots: dependencies: is-number: 7.0.0 + toidentifier@1.0.1: {} + tr46@1.0.1: dependencies: punycode: 2.3.1 @@ -2778,16 +3444,26 @@ snapshots: - supports-color - ts-node + type-is@2.1.0: + dependencies: + content-type: 2.0.0 + media-typer: 1.1.1 + mime-types: 3.0.2 + typescript@5.0.2: {} undici-types@6.21.0: {} universalify@0.1.2: {} + unpipe@1.0.0: {} + use-sync-external-store@1.2.0(react@18.3.1): dependencies: react: 18.3.1 + vary@1.1.2: {} + vite-node@3.2.1(@types/node@22.15.8)(yaml@2.8.0): dependencies: cac: 6.7.14 @@ -2892,6 +3568,12 @@ snapshots: string-width: 5.1.2 strip-ansi: 7.1.0 + wrappy@1.0.2: {} + yaml@2.8.0: {} + zod-to-json-schema@3.25.2(zod@4.4.3): + dependencies: + zod: 4.4.3 + zod@4.4.3: {} diff --git a/src/cli/index.ts b/src/cli/index.ts index d9cbccd..7b9f930 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -103,19 +103,44 @@ async function init() { process.exit(1); } + const sourceAppPath = path.join(process.cwd(), "src", "app"); + const usesSourceDirectory = await fs + .stat(sourceAppPath) + .then((entry) => entry.isDirectory()) + .catch(() => false); + const appPath = usesSourceDirectory + ? sourceAppPath + : path.join(process.cwd(), "app"); + // Create the static app/api/mcp route - const routePath = path.join(process.cwd(), "app", "api", "mcp"); + const routePath = path.join(appPath, "api", "mcp"); await fs.mkdir(routePath, { recursive: true }); - // Create the route.ts file const routeFilePath = path.join(routePath, "route.ts"); - await fs.writeFile(routeFilePath, ROUTE_TEMPLATE); + try { + await fs.writeFile(routeFilePath, ROUTE_TEMPLATE, { flag: "wx" }); + } catch (error) { + if ( + error instanceof Error && + "code" in error && + error.code === "EEXIST" + ) { + throw new Error( + `MCP route already exists at ${path.relative( + process.cwd(), + routeFilePath, + )}`, + ); + } + throw error; + } console.log(chalk.green("✅ Successfully created MCP route handler!")); - // Detect and use the appropriate package manager - const packageManager = await detectPackageManager(); - await installDependencies(packageManager); + if (program.opts().install) { + const packageManager = await detectPackageManager(); + await installDependencies(packageManager); + } } catch (error) { console.error(chalk.red("Error creating MCP route handler:"), error); process.exit(1); @@ -125,6 +150,7 @@ async function init() { program .name("mcp-handler") .description("Initialize MCP route handler in your Next.js project") + .option("--no-install", "Create the route without installing dependencies") .action(init); program.parse(); diff --git a/src/handler/mcp-api-handler.ts b/src/handler/mcp-api-handler.ts index 916971b..8df3597 100644 --- a/src/handler/mcp-api-handler.ts +++ b/src/handler/mcp-api-handler.ts @@ -1,6 +1,7 @@ import { createMcpHandler as createSdkMcpHandler, McpServer, + originValidationResponse, } from "@modelcontextprotocol/server"; import type { McpEvent, @@ -8,6 +9,7 @@ import type { McpErrorEvent, } from "../lib/log-helper"; import { createEvent } from "../lib/log-helper"; +import { getPublicOrigin } from "../lib/url"; import type { ServerOptions } from "."; /** @@ -40,6 +42,12 @@ export type Config = { * @default false */ verboseLogs?: boolean; + /** + * Additional Origin hostnames allowed to call this handler from a browser. + * The public request hostname is always allowed. Requests without an Origin + * header, such as ordinary server-side MCP clients, are unaffected. + */ + allowedOriginHostnames?: string[]; /** * @deprecated Ignored in 2.x. Mount the handler at the desired route in * your framework instead. @@ -49,7 +57,7 @@ export type Config = { * Callback function that receives MCP events. * This can be used to track analytics, debug issues, or implement custom behaviors. */ - onEvent?: (event: McpEvent) => void; + onEvent?: (event: McpEvent) => void | Promise; /** * @deprecated Ignored in 2.x. The legacy HTTP+SSE transport was removed. @@ -69,7 +77,7 @@ export function initializeMcpApiHandler( serverOptions: ServerOptions = {}, config: Config = {}, ): (req: Request) => Promise { - const { verboseLogs, onEvent } = config; + const { verboseLogs, onEvent, allowedOriginHostnames = [] } = config; const { serverInfo = { @@ -79,18 +87,33 @@ export function initializeMcpApiHandler( ...mcpServerOptions } = serverOptions; + const reportEventError = (error: unknown) => { + if (verboseLogs) { + console.error("MCP onEvent callback error:", error); + } + }; + + const emitEvent = (event: Omit) => { + try { + const pending = onEvent?.(createEvent(event)); + if (pending) { + void pending.catch(reportEventError); + } + } catch (error) { + reportEventError(error); + } + }; + const emitError = (error: Error) => { if (verboseLogs) { console.error("MCP handler error:", error); } - onEvent?.( - createEvent({ - type: "ERROR", - error, - source: "request", - severity: "error", - }), - ); + emitEvent({ + type: "ERROR", + error, + source: "request", + severity: "error", + }); }; // The SDK handler serves the 2026-07-28 protocol (stateless, per-request @@ -110,6 +133,15 @@ export function initializeMcpApiHandler( ); return async function mcpApiHandler(req: Request): Promise { + const publicHostname = new URL(getPublicOrigin(req)).hostname; + const originRejection = originValidationResponse(req, [ + publicHostname, + ...allowedOriginHostnames, + ]); + if (originRejection) { + return originRejection; + } + let method: string | undefined; let parsedBody: unknown; const started = Date.now(); @@ -126,14 +158,12 @@ export function initializeMcpApiHandler( "method" in parsedBody ) { method = String((parsedBody as { method: unknown }).method); - onEvent?.( - createEvent({ - type: "REQUEST_RECEIVED", - method, - parameters: parsedBody, - status: "success", - }), - ); + emitEvent({ + type: "REQUEST_RECEIVED", + method, + parameters: parsedBody, + status: "success", + }); } } catch { // Malformed JSON is rejected by the SDK handler below. @@ -143,20 +173,28 @@ export function initializeMcpApiHandler( try { // withMcpAuth attaches the verified AuthInfo to this Request. Pass it // explicitly so the SDK exposes it as ctx.http?.authInfo. - const response = await sdkHandler.fetch(req, { + let response = await sdkHandler.fetch(req, { authInfo: req.auth, parsedBody, }); + if (response.status === 405 && !response.headers.has("Allow")) { + const headers = new Headers(response.headers); + headers.set("Allow", "POST"); + response = new Response(response.body, { + status: response.status, + statusText: response.statusText, + headers, + }); + } + if (method) { - onEvent?.( - createEvent({ - type: "REQUEST_COMPLETED", - method, - duration: Date.now() - started, - status: response.ok ? "success" : "error", - }), - ); + emitEvent({ + type: "REQUEST_COMPLETED", + method, + duration: Date.now() - started, + status: response.ok ? "success" : "error", + }); } return response; } catch (error) { diff --git a/tests/auth-wrapper.test.ts b/tests/auth-wrapper.test.ts new file mode 100644 index 0000000..434096b --- /dev/null +++ b/tests/auth-wrapper.test.ts @@ -0,0 +1,102 @@ +import { describe, expect, it, vi } from "vitest"; +import type { AuthInfo } from "@modelcontextprotocol/server"; +import { withMcpAuth } from "../src"; + +const validAuth: AuthInfo = { + token: "valid-token", + clientId: "test-client", + scopes: ["tools:read", "tools:call"], +}; + +function request(headers: HeadersInit = {}) { + return new Request("https://mcp.example/api/mcp", { headers }); +} + +describe("withMcpAuth", () => { + it("requires authentication without invoking the MCP handler", async () => { + const handler = vi.fn(() => new Response("should not run")); + const verifyToken = vi.fn(() => undefined); + const protectedHandler = withMcpAuth(handler, verifyToken, { + required: true, + }); + + const response = await protectedHandler(request()); + + expect(response.status).toBe(401); + expect(response.headers.get("www-authenticate")).toContain( + 'error="invalid_token"', + ); + expect(response.headers.get("www-authenticate")).toContain( + 'resource_metadata="https://mcp.example/.well-known/oauth-protected-resource"', + ); + expect(verifyToken).toHaveBeenCalledWith(expect.any(Request), undefined); + expect(handler).not.toHaveBeenCalled(); + }); + + it("returns 403 when a verified token lacks any required scope", async () => { + const handler = vi.fn(() => new Response("should not run")); + const protectedHandler = withMcpAuth(handler, () => validAuth, { + requiredScopes: ["tools:read", "admin"], + }); + + const response = await protectedHandler( + request({ Authorization: "Bearer valid-token" }), + ); + + expect(response.status).toBe(403); + expect(response.headers.get("www-authenticate")).toContain( + 'error="insufficient_scope"', + ); + expect(response.headers.get("www-authenticate")).toContain( + 'scope="tools:read admin"', + ); + expect(handler).not.toHaveBeenCalled(); + }); + + it("rejects an expired verified token", async () => { + const handler = vi.fn(() => new Response("should not run")); + const protectedHandler = withMcpAuth( + handler, + () => ({ + ...validAuth, + expiresAt: Math.floor(Date.now() / 1000) - 1, + }), + { required: true }, + ); + + const response = await protectedHandler( + request({ Authorization: "Bearer expired-token" }), + ); + + expect(response.status).toBe(401); + expect(response.headers.get("www-authenticate")).toContain( + 'error_description="Token has expired"', + ); + expect(handler).not.toHaveBeenCalled(); + }); + + it("passes only a valid bearer token and its AuthInfo to the handler", async () => { + const observed: Array<{ bearerToken?: string; auth?: AuthInfo }> = []; + const protectedHandler = withMcpAuth( + (req) => { + observed.push({ auth: req.auth }); + return new Response("ok"); + }, + (req, bearerToken) => { + observed.push({ bearerToken, auth: req.auth }); + return validAuth; + }, + { required: true }, + ); + + const response = await protectedHandler( + request({ Authorization: "bEaReR valid-token" }), + ); + + expect(response.status).toBe(200); + expect(observed).toEqual([ + { bearerToken: "valid-token", auth: undefined }, + { auth: validAuth }, + ]); + }); +}); diff --git a/tests/cli.test.ts b/tests/cli.test.ts new file mode 100644 index 0000000..00465c0 --- /dev/null +++ b/tests/cli.test.ts @@ -0,0 +1,63 @@ +import { describe, expect, it } from "vitest"; +import { spawnSync } from "node:child_process"; +import { mkdtemp, mkdir, readFile, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import path from "node:path"; + +const cliPath = path.resolve(import.meta.dirname, "../dist/cli/index.js"); + +async function nextProject(sourceDirectory = false) { + const directory = await mkdtemp(path.join(tmpdir(), "mcp-handler-cli-")); + await writeFile( + path.join(directory, "package.json"), + JSON.stringify({ + private: true, + dependencies: { + next: "16.0.0", + }, + }), + ); + if (sourceDirectory) { + await mkdir(path.join(directory, "src", "app"), { recursive: true }); + } + return directory; +} + +describe("mcp-handler CLI", () => { + it("generates a static MCP route in an app-directory Next.js project", async () => { + const directory = await nextProject(); + + const result = spawnSync(process.execPath, [cliPath, "--no-install"], { + cwd: directory, + encoding: "utf8", + }); + const route = await readFile( + path.join(directory, "app", "api", "mcp", "route.ts"), + "utf8", + ); + + expect(result.status, result.stderr).toBe(0); + expect(route).toContain("import { createMcpHandler } from 'mcp-handler';"); + expect(route).toContain("server.registerTool("); + expect(route).toContain("export { handler as GET, handler as POST };"); + expect(route).not.toContain("[transport]"); + expect(route).not.toContain("basePath"); + }); + + it("uses src/app when present and never overwrites an existing route", async () => { + const directory = await nextProject(true); + const routeDirectory = path.join(directory, "src", "app", "api", "mcp"); + const routePath = path.join(routeDirectory, "route.ts"); + await mkdir(routeDirectory, { recursive: true }); + await writeFile(routePath, "// user-owned route\n"); + + const result = spawnSync(process.execPath, [cliPath, "--no-install"], { + cwd: directory, + encoding: "utf8", + }); + + expect(result.status).not.toBe(0); + expect(result.stderr).toContain("already exists"); + expect(await readFile(routePath, "utf8")).toBe("// user-owned route\n"); + }); +}); diff --git a/tests/e2e.test.ts b/tests/e2e.test.ts index 3ecce9d..abc1f82 100644 --- a/tests/e2e.test.ts +++ b/tests/e2e.test.ts @@ -11,6 +11,8 @@ import { Client, StreamableHTTPClientTransport, } from "@modelcontextprotocol/client"; +import { Client as V1Client } from "@modelcontextprotocol/sdk-v1/client/index.js"; +import { StreamableHTTPClientTransport as V1StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk-v1/client/streamableHttp.js"; import { createMcpHandler } from "../src/index"; import { withMcpAuth } from "../src/auth/auth-wrapper"; @@ -294,6 +296,74 @@ describe("e2e", () => { ); }); + it("should remain compatible with an actual MCP SDK 1.x client", async () => { + const legacyTransport = new V1StreamableHTTPClientTransport( + new URL(endpoint), + ); + const legacyClient = new V1Client({ + name: "sdk-v1-client", + version: "1.26.0", + }); + + await legacyClient.connect(legacyTransport); + const { tools } = await legacyClient.listTools(); + const result = await legacyClient.callTool({ + name: "echo", + arguments: { + message: "Hello from SDK v1", + }, + }); + + expect(tools.map(({ name }) => name)).toEqual(["echo"]); + expect((result.content as Array<{ text: string }>)[0].text).toEqual( + "Tool echo: Hello from SDK v1", + ); + await legacyClient.close(); + }); + + it("isolates auth context across concurrent clients and later unauthenticated calls", async () => { + const callAs = async (token: string) => { + const transport = new StreamableHTTPClientTransport(new URL(endpoint), { + requestInit: { + headers: { + Authorization: `Bearer ${token}`, + }, + }, + }); + const authenticatedClient = new Client( + { + name: `client-${token}`, + version: "1.0.0", + }, + { + capabilities: {}, + }, + ); + await authenticatedClient.connect(transport); + const result = await authenticatedClient.callTool({ + name: "echo", + arguments: { message: "concurrent" }, + }); + await authenticatedClient.close(); + return (result.content as Array<{ text: string }>)[0].text; + }; + + const [first, second] = await Promise.all([ + callAs("FIRST_TOKEN"), + callAs("SECOND_TOKEN"), + ]); + const unauthenticated = await client.callTool({ + name: "echo", + arguments: { message: "after auth" }, + }); + + expect(first).toEqual("Tool echo: concurrent for FIRST_TOKEN"); + expect(second).toEqual("Tool echo: concurrent for SECOND_TOKEN"); + expect( + (unauthenticated.content as Array<{ text: string }>)[0].text, + ).toEqual("Tool echo: after auth"); + }); + it("should serve the 2026-07-28 protocol to a pinned modern client", async () => { const modernTransport = new StreamableHTTPClientTransport( new URL(endpoint), @@ -359,6 +429,7 @@ describe("e2e", () => { }); it("should return an invalid token error when verifyToken fails", async () => { + const consoleError = vi.spyOn(console, "error").mockImplementation(() => {}); const authenticatedTransport = new StreamableHTTPClientTransport( new URL(endpoint), { @@ -382,9 +453,19 @@ describe("e2e", () => { throw new Error("JWT signature failed, or something"); }); - await expect( - authenticatedClient.connect(authenticatedTransport), - ).rejects.toThrow("Invalid token"); + try { + await expect( + authenticatedClient.connect(authenticatedTransport), + ).rejects.toThrow("Invalid token"); + expect(consoleError).toHaveBeenCalledWith( + "Unexpected error authenticating bearer token:", + expect.objectContaining({ + message: "JWT signature failed, or something", + }), + ); + } finally { + consoleError.mockRestore(); + } }); }); diff --git a/tests/handler.test.ts b/tests/handler.test.ts new file mode 100644 index 0000000..002f262 --- /dev/null +++ b/tests/handler.test.ts @@ -0,0 +1,257 @@ +import { describe, expect, it, vi } from "vitest"; +import { createMcpHandler } from "../src"; + +function legacyInitializeRequest( + url: string, + headers: Record = {}, +) { + return new Request(url, { + method: "POST", + headers: { + Accept: "application/json, text/event-stream", + "Content-Type": "application/json", + ...headers, + }, + body: JSON.stringify({ + jsonrpc: "2.0", + id: 1, + method: "initialize", + params: { + protocolVersion: "2025-11-25", + capabilities: {}, + clientInfo: { name: "raw-test-client", version: "1.0.0" }, + }, + }), + }); +} + +describe("createMcpHandler HTTP boundary", () => { + it("rejects a cross-origin browser request before processing MCP", async () => { + const handler = createMcpHandler(() => {}); + + const response = await handler( + legacyInitializeRequest("http://localhost:3000/custom-mcp", { + Origin: "https://attacker.example", + }), + ); + + expect(response.status).toBe(403); + expect(await response.json()).toMatchObject({ + jsonrpc: "2.0", + error: { + code: -32000, + }, + id: null, + }); + }); + + it("accepts the public request origin and explicitly allowed browser origins", async () => { + const handler = createMcpHandler( + () => {}, + {}, + { allowedOriginHostnames: ["app.example"] }, + ); + + const sameOriginResponse = await handler( + legacyInitializeRequest("https://mcp.example/custom", { + Origin: "https://mcp.example", + }), + ); + const configuredOriginResponse = await handler( + legacyInitializeRequest("https://mcp.example/custom", { + Origin: "https://app.example", + }), + ); + + expect(sameOriginResponse.status).toBe(200); + expect(configuredOriginResponse.status).toBe(200); + }); + + it("rejects POST requests without an application/json media type", async () => { + const handler = createMcpHandler(() => {}); + const response = await handler( + new Request("https://mcp.example/custom", { + method: "POST", + headers: { "Content-Type": "text/plain" }, + body: "{}", + }), + ); + + expect(response.status).toBe(415); + expect(await response.json()).toMatchObject({ + jsonrpc: "2.0", + error: { + code: -32000, + message: + "Unsupported Media Type: Content-Type must be application/json", + }, + }); + }); + + it("returns a JSON-RPC parse error for malformed JSON", async () => { + const handler = createMcpHandler(() => {}); + const response = await handler( + new Request("https://mcp.example/custom", { + method: "POST", + headers: { + Accept: "application/json, text/event-stream", + "Content-Type": "application/json", + }, + body: '{"jsonrpc":"2.0",', + }), + ); + + expect(response.status).toBe(400); + expect(await response.json()).toMatchObject({ + jsonrpc: "2.0", + error: { + code: -32700, + message: expect.stringContaining("Parse error"), + }, + }); + }); + + it.each(["GET", "DELETE"])( + "returns 405 for stateless legacy %s session operations", + async (method) => { + const handler = createMcpHandler(() => {}); + const response = await handler( + new Request("https://mcp.example/custom", { method }), + ); + + expect(response.status).toBe(405); + expect(response.headers.get("allow")).toBe("POST"); + }, + ); + + it("serves the framework mount path even when deprecated route config disagrees", async () => { + const handler = createMcpHandler( + () => {}, + {}, + { + basePath: "/ignored", + streamableHttpEndpoint: "/also-ignored", + }, + ); + + const response = await handler( + legacyInitializeRequest("https://mcp.example/anything/the-app-mounts"), + ); + + expect(response.status).toBe(200); + expect(response.headers.get("content-type")).toContain("text/event-stream"); + }); +}); + +describe("createMcpHandler events", () => { + it("does not fail an MCP request when the analytics callback throws", async () => { + const handler = createMcpHandler( + () => {}, + {}, + { + onEvent: () => { + throw new Error("analytics backend unavailable"); + }, + }, + ); + + const response = await handler( + legacyInitializeRequest("https://mcp.example/custom"), + ); + + expect(response.status).toBe(200); + }); + + it("does not leave an unhandled rejection when an async analytics callback fails", async () => { + const handler = createMcpHandler( + () => {}, + {}, + { + onEvent: async () => { + throw new Error("async analytics backend unavailable"); + }, + }, + ); + + const response = await handler( + legacyInitializeRequest("https://mcp.example/custom"), + ); + await new Promise((resolve) => setTimeout(resolve, 0)); + + expect(response.status).toBe(200); + }); + + it("emits a complete request lifecycle with method, parameters, and duration", async () => { + const onEvent = vi.fn(); + const handler = createMcpHandler( + () => {}, + {}, + { + onEvent, + }, + ); + + const response = await handler( + legacyInitializeRequest("https://mcp.example/custom"), + ); + + expect(response.status).toBe(200); + expect(onEvent).toHaveBeenCalledTimes(2); + expect(onEvent.mock.calls[0][0]).toMatchObject({ + type: "REQUEST_RECEIVED", + method: "initialize", + parameters: { + jsonrpc: "2.0", + id: 1, + method: "initialize", + }, + status: "success", + timestamp: expect.any(Number), + }); + expect(onEvent.mock.calls[1][0]).toMatchObject({ + type: "REQUEST_COMPLETED", + method: "initialize", + duration: expect.any(Number), + status: "success", + timestamp: expect.any(Number), + }); + expect(onEvent.mock.calls[1][0].duration).toBeGreaterThanOrEqual(0); + }); + + it("reports server initialization failures and marks the request as failed", async () => { + const onEvent = vi.fn(); + const handler = createMcpHandler( + () => { + throw new Error("tool registration failed"); + }, + {}, + { + onEvent, + }, + ); + + const response = await handler( + legacyInitializeRequest("https://mcp.example/custom"), + ); + + expect(response.status).toBe(500); + expect(onEvent.mock.calls.map(([event]) => event.type)).toEqual([ + "REQUEST_RECEIVED", + "ERROR", + "REQUEST_COMPLETED", + ]); + expect(onEvent.mock.calls[1][0]).toMatchObject({ + type: "ERROR", + error: expect.objectContaining({ + message: "tool registration failed", + }), + source: "request", + severity: "error", + }); + expect(onEvent.mock.calls[2][0]).toMatchObject({ + type: "REQUEST_COMPLETED", + method: "initialize", + status: "error", + }); + }); +}); diff --git a/tests/package.test.ts b/tests/package.test.ts new file mode 100644 index 0000000..7cfa71f --- /dev/null +++ b/tests/package.test.ts @@ -0,0 +1,81 @@ +import { describe, expect, it } from "vitest"; +import { execFileSync } from "node:child_process"; +import { mkdtemp, mkdir, readFile, rm, stat } from "node:fs/promises"; +import { createRequire } from "node:module"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; + +const projectRoot = path.resolve(import.meta.dirname, ".."); + +describe("published package", () => { + it("contains every public entry point, executable, and linked documentation file", async () => { + const cacheDirectory = path.join(projectRoot, "node_modules", ".cache"); + await mkdir(cacheDirectory, { recursive: true }); + const directory = await mkdtemp( + path.join(cacheDirectory, "mcp-handler-pack-"), + ); + + try { + const tarball = execFileSync( + "pnpm", + ["pack", "--pack-destination", directory], + { + cwd: projectRoot, + encoding: "utf8", + }, + ).trim(); + + execFileSync("tar", ["-xzf", tarball, "-C", directory]); + const packageDirectory = path.join(directory, "package"); + const manifest = JSON.parse( + await readFile(path.join(packageDirectory, "package.json"), "utf8"), + ); + const readme = await readFile( + path.join(packageDirectory, "README.md"), + "utf8", + ); + + const publicFiles = [ + manifest.main, + manifest.bin["mcp-handler"], + ...Object.values(manifest.exports).flatMap((conditions: any) => [ + conditions.import, + conditions.require, + conditions.types.import, + conditions.types.require, + ]), + ]; + for (const relativePath of new Set(publicFiles)) { + await expect( + stat(path.resolve(packageDirectory, relativePath)), + ).resolves.toBeDefined(); + } + + const cli = await stat( + path.resolve(packageDirectory, manifest.bin["mcp-handler"]), + ); + expect(cli.mode & 0o111).not.toBe(0); + + const documentationLinks = [ + ...readme.matchAll(/\]\((docs\/[^)]+)\)/g), + ].map((match) => match[1]); + expect(documentationLinks).not.toHaveLength(0); + for (const relativePath of documentationLinks) { + await expect( + stat(path.resolve(packageDirectory, relativePath)), + ).resolves.toBeDefined(); + } + + const cjs = createRequire(import.meta.url)(packageDirectory); + const esm = await import( + pathToFileURL( + path.resolve(packageDirectory, manifest.exports["."].import), + ).href + ); + expect(cjs.createMcpHandler).toBeTypeOf("function"); + expect(esm.createMcpHandler).toBeTypeOf("function"); + } finally { + await rm(directory, { recursive: true, force: true }); + } + }); +});